PHPを愛する試み 〜self:: parent:: static:: および遅延静的束縛〜
PHPを愛する試みというものを個人的にやっている
今回は、self:: parent:: static:: 遅延静的束縛について図で整理してみた。
スコープ定義演算子 (::)
まず「::」について。これはスコープ定義演算子という。マニュアルには以下のようにある。
スコープ定義演算子 (::)
スコープ定義演算子 (またの名を Paamayim Nekudotayim)、 平たく言うと「ダブルコロン」は、トークンのひとつです。 static, 定数 およびオーバーライドされたクラスのプロパティやメソッドにアクセスすることができます。これらの要素をクラス定義の外から参照する際には、 クラスの名前を使用してください。PHP 5.3.0 以降では、変数を用いてクラスを参照することも可能です。 変数の値に (self や parent、 static といった) キーワードを指定することはできません。
...
http://www.php.net/manual/ja/language.oop5.paamayim-nekudotayim.php
分かりづらいので、自分流に解釈する。例えば、以下の図ように沢山のfoo()があるとして、どのfoo()なのかを決定するための演算子が::である。「::の右側にあるメソッドやプロパティが、::の左側のスコープに属すると決定する演算子」と考えればしっくりくる。
self::やparent::というのは、このスコープを表す方法の1つ。全部で以下の4つがある。
方法 | スコープ | 使える場所 |
---|---|---|
クラス名:: | 明示したクラス | クラス定義の外or中 |
self:: | self::が記載されたクラス | クラス定義の中 |
parent:: | parent::が記載されたクラスの親クラス | クラス定義の中 |
static:: | 直近の "非転送コール" のクラス | クラス定義の中 |
1つずつみていく
クラス名::
クラス名::とすると、スコープは「明示したクラス」となる。
クラス定義の外でも中でも使える。
<?php class C { public static function foo() { D::foo(); //foo()のスコープはD } } class D { public static function foo() { echo 'D_foo' . PHP_EOL; } } C::foo(); //foo()のスコープはC。結果:D_foo
self::
self::とすると、スコープは「self::が記載されたクラス」となる。
クラス定義の中でしか使えない。
<?php class C { public static function foo () { self::bar(); //bar()のスコープはself::が記載されたクラス(C) } public static function bar () { echo 'C_foo' . PHP_EOL; } } class CC extends C { public static function bar () { echo 'CC_foo' . PHP_EOL; } } C::foo(); //C_foo CC::foo(); //C_foo
parent::
parent::とすると、スコープは「parent::が記載されたクラスの親クラス」となる。
クラス定義の中でしか使えない。
<?php class C { public static function bar () { echo 'C_bar' . PHP_EOL; } } class CC extends C { public static function foo () { parent::bar(); //bar()のスコープはparent::が記載されたクラス(CC)の親クラス(C) } public static function bar () { echo 'CC_bar' . PHP_EOL; } } CC::foo(); //C_bar
static::
これが本題。static::とすると、スコープは「直近の "非転送コール" のクラス」となる。
クラス定義の中でしか使えない。
ややこしいのが「直近の "非転送コール" のクラス」というもの。マニュアルにはこう書いてある。
遅延静的束縛 (Late Static Bindings)
...
より正確に言うと、遅延静的束縛は直近の "非転送コール" のクラス名を保存します。 静的メソッドの場合、これは明示的に指定されたクラス (通常は :: 演算子の左側に書かれたもの) となります。静的メソッド以外の場合は、そのオブジェクトのクラスとなります。 "転送コール" とは、self:: や parent::、static:: による静的なコール、 あるいはクラス階層の中での forward_static_call() によるコールのことです。 get_called_class() 関数を使うとコール元のクラス名を文字列で取得できます。 static:: はこのクラスのスコープとなります。
...
http://php.net/manual/ja/language.oop5.late-static-bindings.php
整理すると、
非転送コール: | C::foo()や$c->foo()といったクラス名(もしくはオブジェクト)を明示した呼び出し |
---|---|
転送コール: | self:: parent:: static::もしくはクラス階層の中での forward_static_call() での呼び出し |
遅延静的束縛 | 非転送コール時に、明示されたクラス名(もしくはオブジェクトのクラス名)を保持する機能 |
となる。
これらをふまえて、static::を使ったコードを書いてみる
<?php class C { public static function foo() { static::bar(); // bar()のスコープは直近の "非転送コール" のクラス(C::foo()ならC。CC::foo()ならCC) } public static function bar() { echo 'C_bar' . PHP_EOL; } } class CC extends C { public static function bar() { echo 'CC_bar' . PHP_EOL; } } C::foo(); //C_bar CC::foo(); //CC_bar
C::foo()と呼び出した時とCC::foo()で呼び出した時の動きが変わった!
絵にすると以下のようになる
「直近の非転送コール」とは? 〜例1〜
「直近の非転送コール」が何を表すのか、もう少し詳しくみてみる。
例えば、以下のように途中で転送コール(self::など)をはさんだ場合はどうなるか
<?php class C { public static function foo() { self::hoge(); //self::は転送コール } public static function hoge() { static::bar(); // bar()のスコープは直近の "非転送コール" のクラス(この場合CC) } public static function bar() { echo 'C_bar' . PHP_EOL; } } class CC extends C { public static function bar() { echo 'CC_bar' . PHP_EOL; } } CC::foo(); //CC_bar
転送コールは無視して直近の非転送コールまで遡っていることがわかる
絵にするとこうなる
「直近の非転送コール」とは? 〜例2〜
非転送コールが複数存在する場合はどうか
<?php class C { public static function foo() { C::hoge(); //非転送コール } public static function hoge() { static::bar(); // bar()のスコープは直近の "非転送コール" のクラス(この場合、C) } public static function bar() { echo 'C_bar' . PHP_EOL; } } class CC extends C { public static function bar() { echo 'CC_bar' . PHP_EOL; } } CC::foo(); //C_bar
非転送コールが複数存在する場合は、直近の非転送コールに解決されている
絵にするとこうなる
いずれにおいてもstatic::からコールスタックを遡って、一番近い非転送コール(転送コールは除く)で明示されたクラスに解決されていることがわかる。
静的メソッドの呼び出しではない場合
これまで静的メソッドの呼び出しを例としてきたが、さきほど引用したマニュアルにあるように、「静的メソッド以外の場合は、そのオブジェクトのクラス」となる
<?php class C { public function foo() { static::bar(); } public static function bar() { echo 'C_bar' . PHP_EOL; } } class CC extends C { public static function bar() { echo 'CC_bar' . PHP_EOL; } } $cc = new CC(); $cc->foo(); //CC_bar
静的メソッドでなくても直近の非転送コールまで遡って行く仕組みは同じだ。
これを絵にするとこうなる
注意:「メソッド・プロパティ定義のstatic」「静的変数static」とは違う
static::について混乱するのは、staticが遅延静的束縛以外の意味でも使われるからだ。マニュアルには以下のように書いてある
static キーワード
このページでは、static キーワードを使って静的なメソッドやプロパティを定義する方法を説明します。 static は、 静的な変数の定義 や 静的遅延束縛 にも使えます。これらの場合の static の使い方は、 それぞれのページを参照ください。
http://php.net/manual/ja/language.oop5.static.php
つまり、staticはこれまで見てきたように遅延静的束縛以外に、以下2つの意味で使われる。
・静的なメソッド・プロパティを定義するためのstatic
<?php class C { public static $v = 'hoge'; public static function foo() { echo 'foo' . PHP_EOL; } } var_dump(C::$v); //hoge C::foo(); //foo
・ 静的変数のstatic
<?php class C { public function countup() { static $count; //静的変数 $count++; var_dump($count); } } $c = new C(); $c->countup(); //1 $c->countup(); //2 $c->countup(); //3
同じキーワードが色々な意味で使われるところがややこしい。。。
以上
長かったがstaticについては大分整理できた。
余談
self::は__CLASS__。static::はget_called_class()で参照できる
<?php class C { public static function foo() { var_dump(__CLASS__); //常にC self::bar(); //常にC::foo() var_dump(get_called_class()); //CかCC static::bar(); //C::foo()かCC::foo() } public static function bar() { echo 'C_bar' . PHP_EOL; } } class CC extends C { public static function bar() { echo 'CC_bar' . PHP_EOL; } } C::foo(); CC::foo();
new self() new parent() new static()
self, parent, staticキーワードはオブジェクト生成にも使える。
<?php class C { public static function foo() { var_dump(new self()); } public static function hoge() { var_dump(new static()); } } class CC extends C { public static function bar() { var_dump(new parent()); } } C::foo(); //object(C) CC::bar(); //object(C) C::hoge(); //object(C) CC::hoge(); //object(CC)