遅延静的束縛 (Late Static Bindings)

te Static Bindingsの説明を、まず、見てみましょう。

http://php.net/manual/ja/language.oop5.late-static-bindings.php
PHP 5.3.0 以降、PHP に遅延静的束縛と呼ばれる機能が搭載されます。 これを使用すると、静的継承のコンテキストで呼び出し元のクラスを参照できるようになります。

より正確に言うと、遅延静的束縛は直近の "非転送コール" のクラス名を保存します。 静的メソッドの場合、これは明示的に指定されたクラス (通常は :: 演算子の左側に書かれたもの) となります。静的メソッド以外の場合は、そのオブジェクトのクラスとなります。 "転送コール" とは、self:: や parent::、static:: による静的なコール、 あるいはクラス階層の中での forward_static_call() によるコールのことです。

これでわかったらすごいと思います(笑
もちろん「わかる人もいる」とは思うのですが、一方で「これではわからない人」もいるかと思うので、この文章は「わからない」人のために書いてみたいと思います。

「いまある道具は
 なぜこの形を
 しているのか?」
という文は、UNIX MAGAZINE Classic with DVDの表紙に書かれている文ですが、何度でも繰り返し口にしたい名文だと思っています。

このややこしそうなLate Static Bindings(以降、LSBと略します)も、ちゃんと「理由があって」このような仕様が出来上がっています。

その理由とは。
ものすごく端的には「クラスを継承した時、プログラマが"こう動くんだろうなぁ"という期待に添うように動く事」を目的としています。

例題を出しながら、少しずつ見ていきましょう。

まず「(静的ではない)普通のクラス」と、その継承の動き、を見ていきたいと思います。

<?php

//
class hoge
{
    //
    protected function test_1()
    {
        echo "hoge::test_1\n";
    }
    //
    protected function test_2()
    {
        echo "hoge::test_2\n";
    }
    //
    public function test()
    {
        $this->test_1();
        $this->test_2();
    }
}
//
class foo extends hoge
{
    //
    protected function test_2()
    {
        echo "foo::test_2\n";
    }
}

//
$h_obj = new hoge();
$h_obj->test();

echo "\n";

$f_obj = new foo();
$f_obj->test();

実行すると、以下のようになります。

hoge::test_1
hoge::test_2

hoge::test_1
foo::test_2

簡単に説明をしていきましょう。

クラス hoge については、どの動きもさほど変哲がないかと思われるので、説明は省略します。

クラスfooについて。
「メソッド test()」は、クラスfooには存在しないので、親であるクラスhogeの test()メソッドが使われます。継承の面目躍如ですね。

その test()での処理ですが。
test_1() は、 やはりクラスfooにはないメソッドなので、クラスhogeのメソッドが(ここでも)使われます。
一方でtest_2() は、クラスfooに存在するメソッドなので、こちらはfooのtest_2がcallされます。

慣れないうちは少し追いかけるのに時間がかかるかもしれませんが、落ち着いて読めば、理屈としてはさほど違和感なく読み解いていける、かと思います。

この辺の感覚が「多くのプログラマが、普通に持っているであろう感覚」です。

さて。「ほぼ同じ」コードを、今度は「静的」な文脈で書いてみたいと思います。
コードはほぼ同じになります。
また、 スコープ定義演算子 で少し書きましたが、$thisの代わりに、「自分自身を指し示す」self::を使います。

<?php

//
class hoge
{
    //
    protected static function test_1()
    {
        echo "hoge::test_1\n";
    }
    //
    protected static function test_2()
    {
        echo "hoge::test_2\n";
    }
    //
    public static function test()
    {
        self::test_1();
        self::test_2();
    }
}
//
class foo extends hoge
{
    //
    protected static function test_2()
    {
        echo "foo::test_2\n";
    }
}

//
hoge::test();

echo "\n";

foo::test();

実行してみます。

hoge::test_1
hoge::test_2

hoge::test_1
hoge::test_2

  クラスhogeは省略します。

問題はクラスfooの test_2()メソッド。
なんで呼ばれていないのでしょうか?

少し分解してみます。

クラスfooの test()メソッドをcallしました。
self::test_1();
の、self::は「現在のクラスへの静的参照( http://php.net/manual/ja/language.oop5.late-static-bindings.php )」になります。
なので、test()メソッドが書かれているクラス名「hoge」への参照になるので、「self::test_1();」は「hoge::test_1();」に置き換えられて、解決してきます。

問題なのは。これが
self::test_2();
にも適用されてしまうため、「self::test_2();」は「hoge::test_2();」として解決されていきます。

もちろんこれが「意図通り」であるのなら、なんの問題もないのですが。
staticではない普通のクラスの時の継承と、ちょっと動きが変わってきてしまっているので。人と状況によっては、動きに違和感があったり、或いは「想定外」であったりする事もあるのではないか、と思うのです。

そこで出てきたのがLSBです。
LSBに対応した書き方で、コードを少しだけ書き直してみましょう。

<?php

//
class hoge
{
    //
    static protected function test_1()
    {
        echo "hoge::test_1\n";
    }
    //
    static protected function test_2()
    {
        echo "hoge::test_2\n";
    }
    //
    static public function test()
    {
        static::test_1();
        static::test_2();
    }
}
//
class foo extends hoge
{
    //
    static protected function test_2()
    {
        echo "foo::test_2\n";
    }
}

//
hoge::test();

echo "\n";

foo::test();

ほんの少し、書き換えました。
どこでしょう?????

正解は。testメソッドの
self::

static::
に書き換えました。これだけです。

で、実行。

hoge::test_1
hoge::test_2

hoge::test_1
foo::test_2

一番初めの、「静的ではない普通のクラスの継承」と同じ動きになりました。
………なんででしょう?

少し、かみ砕いてみましょう。

self::は「self:: あるいは __CLASS__ による現在のクラスへの静的参照は、 そのメソッドが属するクラス (つまり、 そのメソッドが定義されているクラス) に解決されます。 」とあります。
つまり「そのメソッドが書かれているクラス」に紐づけられます。……だから困ったんですよね。

static::は「呼び出し元のクラスを参照できる」ようになります。
もうちょっとかみ砕いて。

    static public function test()
    {
        static::test_1();
        static::test_2();
    }

のメソッドを

foo::test();

と呼びました。

「static::test_1();」の時。
staticを解決するために「呼び出し元のクラス」を確認します。「呼び出し元」ってのは「foo::test();」なので、呼び出し元クラスは「foo」ですね。
なので「foo::test_1();」とみなします。
まぁ、fooにtest_1()はないんで、hogeを見に行くんですけどね。

「static::test_2();」の時。
staticを解決するために「呼び出し元のクラス」を確認します。「呼び出し元」ってのは「foo::test();」なので、呼び出し元クラスは「foo」ですね。
なので「foo::test_2();」とみなします。
……と、fooにtest_2()は実装してあるので、test_2()を見に行ってくれるんですね。

このようにして。
今までのPHP(具体的には5.3未満)だとselfしか書けず、しかし継承の文脈で、状況によっては「静的じゃないときと動きが違う」ってんで、結構困ったシーンもあったようです。

そして、それを解消するために、遅延静的束縛 (Late Static Bindings) は生まれました。

一言でまとめると「静的なメソッドで"自分自身"を意味する記法を使いたい時は、self::ではなくて、static::を使うと、幸せになれる可能性が高くなる」ってだけ、のお話なのですが。

割と重要なポイントだったりしますので、ちょいと長々と、コードを重ねていきました。


この記事が気に入ったらサポートをしてみませんか?