先日2021/03/02に、New in initializersというRFCが提出されました。
なにかというと、こんな構文が書けるようになります。
class Test {
private $foo = new Foo();
public function __construct(
private Logger $logger = new NullLogger,
){}
}
PHP RFC: New in initializers
Introduction
このRFCでは、プロパティやパラメータのデフォルト値など、イニシャライザの内部でnew式を使えるようにすることを提案します。
現在のPHPでは、以下のようなコードは許可されていません。
class Test {
public function __construct(
private Logger $logger = new NullLogger,
) {}
}
かわりに、このような書き方をしなければなりません。
class Test {
private Logger $logger;
public function __construct(
?Logger $logger = null,
) {
$this->logger = $logger ?? new NullLogger;
}
}
この文法では、実際のデフォルト値が何であるかがわかりづらく、引数にも本来不要なnull許容型を強制されてしまいます。
このRFCでは、この制限を緩和し、全てのイニシャライザ内でnewを使用できるようにします。
Proposal
イニシャライザ式の一部としてnew構文を許可します。
名前付き引数を含め、コンストラクタ引数として使用可能です。
// 全てOK
function test(
$foo = new A,
$bar = new B(1),
$baz = new C(x: 2),
) {}
動的クラスや無名クラスは許可されません。
引数アンパックや定数式も許可されていません。
// 全てNG
function test(
$a = new (CLASS_NAME_CONSTANT)(), // 動的クラス
$b = new class {}, // 無名クラス
$c = new A(...[]), // 引数アンパック
$d = new B($abc), // 定数式?
) {}
影響を受けるのは、静的変数、定数およびクラス定数、プロパティ、引数のデフォルト値、そしてアトリビュートの引数です。
static $x = new Foo;
const C = new Foo;
#[AnAttribute(new Foo)]
class Test {
const C = new Foo;
public static $prop = new Foo;
public $prop = new Foo;
}
function test($param = new Foo) {}
Order of evaluation
イニシャライザ式は、これまでは常にオートローダやエラーハンドラを通して副作用を制御することができました。
しかし、newをサポートすることで、副作用が第一級オブジェクトとなるため、どのような順番で評価されるかに注意を払う必要があります。
評価順は、イニシャライザの種類によって異なります。
- 静的変数イニシャライザは、制御フローが宣言に到達した時点で評価されます。
- グローバル定数のイニシャライザは、制御フローが宣言に到達した時点で評価されます。
- アトリビュート引数は、
ReflectionAttribute::getArguments()
もしくはReflectionAttribute::newInstance()
が呼ばれるたびに、左から順に評価されます。 - 引数デフォルト値は、引数を渡さず関数を呼び出すたびに、左から順に評価されます。
- プロパティデフォルト値は、オブジェクトをインスタンス化したときに、宣言した順に評価されます。これはコンストラクタが呼ばれる前に行われ、また評価中に例外がスローされた場合はデストラクタは呼ばれません。
- 静的プロパティとクラス定数の評価順は不定です。現在は、該当のクラスが初めて使用されたときに評価されます。
Interaction with reflection
イニシャライザには、リフレクションを通してアクセスすることが可能です。
-
ReflectionFunctionAbstract::getStaticVariables()
静的変数の現在値を返します。まだ到達していない場合は即時評価します。 -
ReflectionParameter::getDefaultValue()
呼び出しごとにデフォルト値を評価します。 -
ReflectionParameter::isDefaultValueConstant()
・ReflectionParameter::getDefaultValueConstantName()
現在値を評価しません。 -
ReflectionClassConstant::getValue()
・ReflectionClass::getConstants()
・ReflectionClass::getConstant()
クラス定数を返します。まだ到達していない場合は即時評価します。 -
ReflectionClass::getDefaultProperties()
・ReflectionProperty::getDefaultValue()
呼び出しごとに、静的プロパティと非静的プロパティを両方とも評価します。 -
ReflectionAttribute::getArguments()
・ReflectionAttribute::newInstance()
呼び出しごとにアトリビュート引数を評価します。 -
ReflectionObject::newInstanceWithoutConstructor()
デフォルト値を評価して代入します。
Recursion protection
デフォルト値が再起になった場合は例外をスローします。
class Test {
public $test = new Test;
}
new Test; // Error: Trying to recursively instantiate Test while evaluating default value for Test::$test
Trait property compatibility
同じプロパティの定義された複数のtraitを使用した場合、互換性チェックが行われ、両方に同じイニシャライザを要求されます。
trait T1 {
public $prop = new A;
}
trait T2 {
public $prop = new A;
}
class B {
use T1, T2;
}
これは禁止されます。
なぜなら互換性の比較は===
で行われるため、T1とT2のプロパティは異なるインスタンスと判断されるからです。
しかし、互換性チェックにおいて実際にnew構文を走らせるようなことはしたくないし、それによって副作用が発生する可能性もあります。
イニシャライザ式は大きくふたつ、静的と動的に分かれます。
静的は既存の全ての型で、動的はnewなどです。
traitのイニシャライザが動的である場合、常に互換性はないとみなされます。
Backward Incompatible Changes
後方互換性のない変更はありません。
Future Scope
このRFCは、newをサポートするだけという小さなものです。
しかし、同様の呼び出し式をサポートするための技術的基礎を築くものでもあります。
感想
いやあデフォルト値ありがたいですね。
引数デフォルト値が書けるようになることで、いちいちメソッド内で引数によって分岐する無駄が避けられるようになります。
またプロパティデフォルト値にはこれまでプリミティブ型しか設定できず、オブジェクトには設定できないという非常に半端な状態でしたが、この変更によって自然にデフォルト値が書けるようになります。
そういえば自身のネストは例外って書いてましたが、互いに持ち合わせた場合はどうなるのでしょうかね。
class A{
private $b = new B();
}
class B{
private $a = new A();
}
先日提出されたばかりのRFCなので、今後このような考慮漏れの調査および問題点の調整などを行い、その後投票で2/3以上の賛成という長いプロセスを経ないかぎり、PHP本体に取り込まれることはありません。
MLではさっそく様々な議論が交わされています。
しかし便利な機能であり、既存のPHP文法と矛盾せず自然に文法に組み込めること、さらに既にプルリクが存在すること、そして何よりAuthorがNikitaなので、そのうち受理されると思います。