Quantcast
Channel: rfcタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 193

【PHP8.0】PHPにオブジェクト初期化子が導入される

$
0
0

これまで何度も塩漬けにされたり却下されたりしていたオブジェクト初期化子ですが、ついにPHP8.0で導入されることになりました。
オブジェクト初期化子が何かというとこれです。

classHOGE{publicfunction__construct(privateint$x){// $HOGE->xが生える}}

これはオブジェクト初期化子でいいのか?

日本語で何と表すのか適切な単語が思いつかなかったのでとりあえずオブジェクト初期化子としておきます。
愚直に訳すと"コンストラクタ引数昇格"ですが、そんな単語は無いうえに型昇格と紛らわしいです。
引数プロパティ宣言パラメータプロパティ宣言もほぼ使われてないし何と表現すればいいのだろう。
きっと誰かが適切な語をプルリクしてくれるはず。

以下は該当のRFC、PHP RFC: Constructor Property Promotionの日本語訳です。

PHP RFC: Constructor Property Promotion

Introduction

PHPでは現在のところ、オブジェクトにプロパティを定義するだけでも同じことを複数回書かなければならないため、多くの無駄が必要です。
以下の単純なクラスを考えてみましょう。

classPoint{publicfloat$x;publicfloat$y;publicfloat$z;publicfunction__construct(float$x=0.0,float$y=0.0,float$z=0.0,){$this->x=$x;$this->y=$y;$this->z=$z;}}

プロパティの表記は、1:プロパティの宣言、2:コンストラクタの引数、3:プロパティの代入で3回も繰り返されます。
さらにプロパティの型も2箇所に書かなければなりません。

プロパティ宣言とコンストラクタ以外には何も含まれていないバリューオブジェクトでは特に、多くの重複によって変更が複雑になり、エラーを起こしやすいものとなります。

このRFCでは、プロパティの定義とコンストラクタを組み合わせるショートハンド構文の導入を提案します。

PHP8
classPoint{publicfunction__construct(publicfloat$x=0.0,publicfloat$y=0.0,publicfloat$z=0.0,){}}

このショートハンド構文は、前述の例と厳密に同じで、より短く書くことができます。
構文は姉妹言語Hackから採用しています。

Proposal

コンストラクタの引数にpublic/protected/private何れかが記述されている場合、その引数は"promoteされた引数"とします。
promoteされた引数には、同じ名前のプロパティが追加され、値が割り当てられます。

Constraints

promoteはabstractではないクラスのコンストラクタでのみ記述可能です。
従って、以下のような構文は使用不能です。

// エラー:コンストラクタではないfunctiontest(private$x){}abstractclassTest{// エラー:abstractなので駄目abstractpublicfunction__construct(private$x);}interfaceTest{// エラー:interfaceも駄目publicfunction__construct(private$x);}

一般的でない使い方ですが、トレイトでは使用可能です。

対応する可視性キーワードはpublic/protected/privateのみです。

classTest{// エラー:varはサポートしてないpublicfunction__construct(var$prop){}}

promoteされた引数によるプロパティは、通常のプロパティと全く同じ扱いになります。
特に注意点として、同じプロパティを二度宣言することはできません。

classTest{public$prop;// Error: Redeclaration of property.publicfunction__construct(public$prop){}}

また、プロパティにすることのできないcallable型は使用することができません。

classTest{// Error: Callable type not supported for properties.publicfunction__construct(publiccallable$callback){}}

promoteされたプロパティはプロパティ宣言と同義であるため、デフォルトがNULLの場合はNULL許容型を明示しなければなりません。

classTest{// Error: Using null default on non-nullable propertypublicfunction__construct(publicType$prop=null){}// こっちはOKpublicfunction__construct(public?Type$prop=null){}}

可変長引数をpromoteすることはできません。

classTest{// エラーpublicfunction__construct(publicstring...$strings){}}

理由としては、明示する引数の型(ここではstring)と、実際に渡される引数の型(ここではstringの配列)が異なるからです。
$stringsプロパティをstringの配列にすることも可能ですが、それではわかりづらくなります。

promoteプロパティと明示的なプロパティ宣言を組み合わせることは可能です。
またpromoteプロパティとpromoteされない引数を同時に渡すことも可能です。

// 正しいclassTest{publicstring$explicitProp;publicfunction__construct(publicint$promotedProp,int$normalArg){$this->explicitProp=(string)$normalArg;}}

Desugaring

promoteプロパティはただのシンタックスシュガーであり、全てのpromoteプロパティに対して以下の変換が適用されます。

// シンタックスシュガーclassTest{publicfunction__construct(publicType$prop=DEFAULT){}}// こう展開されるclassTest{publicType$prop;publicfunction__construct(Type$prop=DEFAULT){$this->prop=$prop;}}

自動的に宣言されるプロパティの可視性と型は、promoteプロパティの可視性および型と同じになります。
注目すべき点は、プロパティにデフォルト値は適用されず(つまり、未初期化で始まります)、コンストラクタ引数でのみ指定されるところです。

プロパティ宣言時にもデフォルト値を指定したほうがよいようにも思えますが、将来的にデフォルト値で指定することが望ましくなるであろう理由が存在します。

ひとつめは、プロパティのデフォルト値に任意の式を利用できるようにする拡張の可能性です。

// FROMclassTest{publicfunction__construct(publicDependency$prop=newDependency()){}}// TOclassTest{publicDependency$prop/* = new Dependency() */;publicfunction__construct(Dependency$prop=newDependency()){$this->prop=$prop;}}

こうなると、プロパティ宣言時とデフォルト値でオブジェクトを2回構築することとなるため望ましくありません。

また、新潟アクセス修正子のルールではプロパティでデフォルト値を宣言すると、コンストラクタで代入することもできなくなります。

promote引数が参照であった場合、プロパティも参照になります。

// FROMclassTest{publicfunction__construct(publicarray&$array){}}// TOclassTest{publicarray$array;publicfunction__construct(array&$array){$this->array=&$array;}}

promoteプロパティへの引数の割り当ては、コンストラクタの冒頭で行われます。
従って、コンストラクタ内でも引数とプロパティの両方にアクセスすることが可能です。

// 動作するclassPositivePoint{publicfunction__construct(publicfloat$x,publicfloat$y){assert($x>=0.0);assert($y>=0.0);}}// こっちも動作するclassPositivePoint{publicfunction__construct(publicfloat$x,publicfloat$y){assert($this->x>=0.0);assert($this->y>=0.0);}}

Reflection

リフレクションおよびその他の解析機構で見ると、シンタックスシュガーを解除した後の状態になります。
すなわち、promoteプロパティは明示的に宣言されたプロパティのように見え、promote引数は通常のコンストラクタ引数のように見える、ということです。

PHPは引数に関するDocコメントを公開していませんが、promoteプロパティのDocコメントも保持されます。

classTest{publicfunction__construct(/** @SomeAnnotation() */public$annotatedProperty){}}$rp=newReflectionProperty(Test::class,'annotatedProperty');echo$rp->getDocComment();// "/** @SomeAnnotation */"

この例のように、promoteプロパティではDocコメントベースのアノテーションを使用することができます。

また、2メソッドが追加されます。

ReflectionProperty::isPromoted()は、promoteプロパティであればtrueを返します。
ReflectionParameter::isPromoted()は、promote引数であればtrueを返します。

プロパティがpromoteされたかどうかを気にする場面はほとんど存在しないと思われますが、この情報によって元のコードをより簡単に再構築することができます。

Inheritance

オブジェクト初期化子は継承することができますが、特に特筆すべきようなことはありません。
abstrautを含む典型的な継承のユースケースを以下に示します。

abstractclassNode{publicfunction__construct(protectedLocation$startLoc=null,protectedLocation$endLoc=null,){}}classParamNodeextendsNode{publicfunction__construct(publicstring$name,publicExprNode$default=null,publicTypeNode$type=null,publicbool$byRef=false,publicbool$variadic=false,Location$startLoc=null,Location$endLoc=null,){parent::__construct($startLoc,$endLoc);}}

ParamNodeクラスでいくつかのpromoteプロパティを宣言し、さらに二つの普通の引数を親コンストラクタに転送しています。
これは以下のように展開されます。

abstractclassNode{protectedLocation$startLoc;protectedLocation$endLoc;publicfunction__construct(Location$startLoc=null,Location$endLoc=null,){$this->startLoc=$startLoc;$this->endLoc=$endLoc;}}classParamNodeextendsNode{publicstring$name;publicExprNode$default;publicTypeNode$type;publicbool$byRef;publicbool$variadic;publicfunction__construct(string$name,ExprNode$default=null,TypeNode$type=null,bool$byRef=false,bool$variadic=false,Location$startLoc=null,Location$endLoc=null,){$this->name=$name;$this->default=$default;$this->type=$type;$this->byRef=$byRef;$this->variadic=$variadic;parent::__construct($startLoc,$endLoc);}}

プロパティへの代入は、親コンストラクタが呼ばれる前に行われることに注意してください。
これはコーディングスタイルとして一般的ではありませんが、動作に影響が出るようなことはほぼありません。

Attributes

PHP8ではアトリビュートも導入されるため、相互作用を考慮する必要があります。
アトリビュートは、プロパティと引数の両方で使用することができます。

classTest{publicfunction__construct(<<ExampleAttribute>>publicint$prop,){}}

このコードがどのように解釈されるか決める必要があります。
1. アトリビュートは引数にのみ適用する。
2. アトリビュートはプロパティにのみ適用する。
3. アトリビュートは引数とプロパティの両方に適用する。
4. 曖昧さを避けるためエラーにする

// Option 1: アトリビュートは引数にのみ適用するclassTest{publicint$prop;publicfunction__construct(<<ExampleAttribute>>int$prop,){}}// Option 2: アトリビュートはプロパティにのみ適用するclassTest{<<ExampleAttribute>>publicint$prop;publicfunction__construct(int$prop,){}}// Option 3: アトリビュートは引数とプロパティの両方に適用するclassTest{<<ExampleAttribute>>publicint$prop;publicfunction__construct(<<ExampleAttribute>>int$prop,){}}// Option 4: 曖昧さを避けるためエラーにする

このRFCでは3番目、つまり引数とプロパティの両方に適用することを提案しています。
これが最も柔軟性の高い方法だからです。

ただし、これは実装に依ると考えています。
PHP8の実装に関わる作業で、アトリビュートをプロパティにのみ配置した方がよいと判明した場合は、そのように変更される場合があります。

Coding Style Consideration

このセクションではコーディングスタイルの推奨について解説します。
規程ではありません。

promoteプロパティを使用する場合、コンストラクタをクラス最初のメソッドとして、明示的なプロパティ宣言の直後に配置することをお勧めします。
これにより、全ての全てのプロパティが先頭にまとめられ、一目でわかるようになります。
静的メソッドを最初に配置することを要求しているコーディング規約は、コンストラクタを最初に配置するよう規約を調整する必要があります。

promoteプロパティに@paramアノテーションを使用している場合、ドキュメントツールは@varアノテーションも含まれているものとして解釈されるべきです。

// 元のコードclassPoint{/**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */publicfunction__construct(publicfloat$x=0.0,publicfloat$y=0.0,publicfloat$z=0.0,){}}// こう解釈するclassPoint{/**
     * @var float $x The X coordinate.
     */publicfloat$x;/**
     * @var float $y The Y coordinate.
     */publicfloat$y;/**
     * @var float $z The Z coordinate.
     */publicfloat$z;/**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */publicfunction__construct(float$x=0.0,float$y=0.0,float$z=0.0,){$this->x=$x;$this->y=$y;$this->z=$z;}}

最後に、promoteプロパティは、あくまで一般的なケースをカバーするための便利な省略記法であるに過ぎないことに注意してください。
promoteプロパティはいつでも明示的なプロパティに書き換えることができます。
そのため、この変更は下位互換性を壊すことはありません。

Backward Incompatible Changes

下位互換性のない変更はありません。

Future Scope

Larryが、この機能と他の機能を組み合わせることによってオブジェクト初期化を改善する方法について、より深いビジョンを提供しています

Prior Art

この機能、あるいは類似した機能は多くの言語でサポートされています。
Hack
TypeScript
Kotlin

先行するRFCが存在します。
Automatic property initialization プロパティ宣言は必要とする、より弱い形です。
Constructor Argument Promotion このRFCとほとんど同じです。
Code free constructor Kotlinの文法に基づいています。

Vote

投票期間は2020/05/29まで、2/3+1の賛成が必要です。
このRFCは賛成46反対10で受理されました。

感想

プロパティを書くのが格段に楽になりますね。
後から見るときにプロパティが宣言されているのかどうかちょっとわかりにくそうですが、この機能が使えるのはコンストラクタだけなのでそこだけ抑えていれば大丈夫でしょう。
コンストラクタだけではなく任意のメソッドで使えると便利では、と一瞬思ったものの、これを無制限に使えると完全に収拾が付かなくなってしまうので、やはりコンストラクタだけに留めておくのが賢明そうですね。


Viewing all articles
Browse latest Browse all 193

Trending Articles