对 PHP 后期静态绑定的理解

什么是后期静态绑定

在看一些框架源码或者是某个项目的代码时,经常能看到后期静态绑定的用法
。比如下面这段:

public static function getInstance()
{
    if (is_null(static::$instance)) {
        static::$instance = new static;
    }
    return static::$instance;
}

这里用到的就是后期静态绑定。那么,什么是后期静态绑定?

“后期绑定”的意思是说,static:: 不再被解析为定义当前方法所在的类,而是在实际运行时计算的。

这里要先说两个概念,一个是转发调用,另一个是非转发调用。

  • 转发调用
    所谓的“转发调用”(forwarding call)指的是通过以下几种方式进行的静态调用:self::, parent::, static:: 以及 forward_ static _call()。即在进行静态调用时未指名类名的调用属于转发调用。

  • 非转发调用
    非转发调用其实就是明确指定类名的静态调用(foo::bar())和非静态调用($foo->bar())。即明确地指定类名的静态调用和非静态调用。

顾名思义,非转发调用前面有类名所以调用的函数一定是属于“这个类的”,不需要转到别的类。转发调用就是由于前期的静态绑定导致在后面调用静态方法时可能“转发到其他的类”

在PHP的官方文档里,对于后期静态绑定是这样说的:后期静态绑定工作原理是存储了在上一个“非转发调用”(non-forwarding call)中的类名。意思是当我们调用一个转发调用的静态调用时,实际调用的类是上一个非转发调用的类。

来看两个例子:

例1:

class A {
    public static function who() {
        echo __CLASS__;
    }
    public static function test() {
        static::who(); // 后期静态绑定从这里开始
    }
}
class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}
B::test();

以上代码会输出

B

例2:

class A {
    public static function foo() {
        static::who();
    }

    public static function who() {
        echo __CLASS__."\n";
    }
}

class B extends A {
    public static function test() {
        A::foo();
        parent::foo();
        self::foo();
    }

    public static function who() {
        echo __CLASS__."\n";
    }
}
class C extends B {
    public static function who() {
        echo __CLASS__."\n";
    }
}

C::test();

以上代码会输出

A
C
C

在这里主要分析下例2。

1.C::test(),这是一个非转发调用,因为::前面有类名C。

2.进入test()方法,有三个静态调用 A::foo(),parent::foo(),self::foo(),对于这三个静态调用来说,他们的非转发调用类就是 C。

3.现在执行A::foo(),这是一个非转发调用。A::foo()中的代码是 static::who(),这是一个转发调用,对于这个转发调用来说他的非转发调用类就是不再是C而是A(因为之前执行了A::foo())。因此执行的结果为A

4.现在执行 parent::foo(),这是一个转发调用,转发到哪里呢?就是它的上一个非转发调用的类,也就是类C(在步骤2中提到的)。在这里一定要注意虽然在这之前执行了 A::foo(),但是parent::foo()的上一个非转发调用的类任然是类C。因此执行的结果是 C.

5.现在执行 self::foo(),这个和 parent::foo()一样都是转发调用,因此也输出 C。

使用后期静态绑定的好处

后期静态绑定目前我看到较多的是用于对象实例化中,在实例化对象时,static 会根据运行时调用的类来决定实例化对象,而 self 则是根据所在位置的类来决定实例化对象。当我们只想实例化子类,并且不希望后续在对子类的使用中由于父类的变化对子类产生影响时,后期静态绑定就能发挥它的作用了。

本文章首发在 Laravel China 社区

临渊羡鱼,退而结网。