对 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 则是根据所在位置的类来决定实例化对象。当我们只想实例化子类,并且不希望后续在对子类的使用中由于父类的变化对子类产生影响时,后期静态绑定就能发挥它的作用了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
You can not connect the dots looking forward, you can only connect them looking backwards.
本帖由系统于 6年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 6
Silencewj

非转发调用 和 转发调用 的概念有助于我理解,谢谢。

5年前 评论

解释的很详细 感谢!

4年前 评论

非转发调用 和 转发调用 的概念很有帮助,谢谢楼主

4年前 评论

解答了我一直对后期静态绑定的疑惑 :+1:

3年前 评论

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

觉得这句理解不是很恰当,转发调用的意思是:遇到parend:: self:: 等 会把上一次存储的非转发调用的信息 转发给被调用的方法。

3年前 评论

说下自己的理解,先看官方怎么解释的
More precisely, late static bindings work by storing the class named in the last “non-forwarding call”.In case of static method calls, this is the class explicitly named (usually the one on the left of the :: operator)
这句话大致意思是延迟静态绑定 是通过 存储最后一次的非转发调用的类名来工作的, 也就是说延迟静态绑定的那个类就是最后一次的非转发调用的类名;
最后一次的非转发调用的类名,这个举个例子:
ClassC::test()
test方法又调用一个非转发的静态调用,假设为ClassA::foo()
然后foo又调用了一个转发调用,那么这个转发调用绑定的类是哪一个呢,就是最后一次调用它的类,就是ClassA
那么上面的例子就非常好理解了自然是输出A C C

ps:这个不要被多层函数调用迷惑,跟这个没有关系,只跟类有关系

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!