为什么你不能从一个尚未定义的 class 继承,它继承自一个尚未定义的 class?

Why can't you inherit from a not-yet-defined class which inherits from a not-yet-defined class?

我研究 class 编译,它的顺序和逻辑。

如果我在简单父级之前声明一个 class:

 class First extends Second{}
 class Second{}

这样就可以了。 See live example across PHP versions.

但如果父级 class 也有一些尚未声明的父级(扩展或实现),如本例所示:

class First extends Second{}
class Second extends Third{}
class Third{}

我会报错:

Fatal error: Class 'Second' not found ...

See live example across PHP versions.

那么,为什么在第二个示例中找不到 Second class? 也许 php 无法编译这个 class 因为它还需要编译 Third class,或者什么?

我试图找出为什么在第一个例子中,PHP 编译 class 第二个,但是如果它有一些父 classes,它不会。我研究了很多,但一无所获。

所以,PHP 使用了一个叫做 "late binding" 的东西。基本上,继承和 class 定义直到文件编译结束才会发生。

这有很多原因。第一个是您展示的示例(first extends second {} 有效)。第二个原因是opcache。

为了使编译在 opcache 领域中正常工作,编译必须在没有来自其他编译文件的状态的情况下进行。这意味着在编译文件时,class 符号 table 被清空。

然后,缓存该编译的结果。然后在运行时,当从内存加载编译文件时,opcache 运行后期绑定,然后进行继承并实际声明 classes.

class First {}

当看到 class 时,它会立即添加到符号 table 中。无论它在文件中的哪个位置。因为不需要后期绑定任何东西,所以它已经完全定义好了。这种技术称为 早期绑定 ,它允许您在声明之前使用 class 或函数。

class Third extends Second {}

看到它时,它已编译,但实际上并未声明。相反,它被添加到 "late binding" 列表中。

class Second extends First {}

当最终看到它时,它也是编译的,并没有实际声明。它已添加到后期绑定列表中,但 after Third.

所以现在,当后期绑定过程发生时,它会一个接一个地遍历 "late bound" classes 的列表。它看到的第一个是 Third。然后它试图找到 Second class,但找不到(因为它还没有实际声明)。所以报错了。

如果重新排列 classes:

class Second extends First {}
class Third extends Second {}
class First {}

然后你会看到它工作正常。

为什么要这样做???

嗯,PHP 很有趣。让我们想象一系列文件:

<?php // a.php
class Foo extends Bar {}

<?php // b1.php
class Bar {
    //impl 1
}

<?php // b2.php
class Bar {
    //impl 2
}

现在,您获得哪一端 Foo 实例将取决于您加载了哪个 b 文件。如果您需要 b2.php,您将得到 Foo extends Bar (impl2)。如果您需要 b1.php,您将得到 Foo extends Bar (impl1)

通常我们不会以这种方式编写代码,但在某些情况下可能会发生这种情况。

在正常的 PHP 请求中,这很容易处理。原因是我们在编译Foo的时候可以知道Bar。所以我们可以相应地调整我们的编译过程。

但是当我们将操作码缓存加入其中时,事情就变得复杂多了。如果我们用 b1.php 的全局状态编译 Foo,然后稍后(在不同的请求中)切换到 b2.php,事情就会以奇怪的方式中断。

因此,操作码会在编译文件之前将全局状态缓存为空。因此 a.php 将被编译为就好像它是应用程序中的唯一文件一样。

编译完成后,它被缓存到内存中(供以后的请求重用)。

然后,在那一点之后(或在未来的请求中从内存中加载之后),"delayed" 步骤发生。然后将编译后的文件与请求的状态相结合。

这样,opcache 可以更有效地将文件缓存为独立实体,因为绑定到全局状态是在读取缓存之后发生的。

源代码。

为什么要看源码呢

Zend/zend_compile.c中我们可以看到编译class的函数:zend_compile_class_decl()。大约一半的地方你会看到下面的代码:

if (extends_ast) {
    opline->opcode = ZEND_DECLARE_INHERITED_CLASS;
    opline->extended_value = extends_node.u.op.var;
} else {
    opline->opcode = ZEND_DECLARE_CLASS;
}

所以它最初发出一个操作码来声明继承的class。然后,在编译发生后,调用一个名为 zend_do_early_binding() 的函数。这会在文件中预先声明函数和 classes(因此它们在顶部可用)。对于正常的 classes 和函数,它只是将它们添加到符号 table(声明它们)。

有趣的是在继承的情况下:

if (((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) == NULL) ||
    ((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) &&
    (ce->type == ZEND_INTERNAL_CLASS))) {
    if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
        uint32_t *opline_num = &CG(active_op_array)->early_binding;

        while (*opline_num != (uint32_t)-1) {
            opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
        }
        *opline_num = opline - CG(active_op_array)->opcodes;
        opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
        opline->result_type = IS_UNUSED;
        opline->result.opline_num = -1;
    }
    return;
}

外层 if 主要是尝试从符号 table 中获取 class 并检查它是否不存在。第二个 if 检查我们是否使用延迟绑定(启用 opcache)。

然后,它将声明class的操作码复制到延迟的早期绑定数组中。

最后,函数zend_do_delayed_early_binding()被调用(通常由一个opcache),它循环遍历列表并实际绑定继承的classes:

while (opline_num != (uint32_t)-1) {
    zval *parent_name = RT_CONSTANT(op_array, op_array->opcodes[opline_num-1].op2);
    if ((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) != NULL) {
        do_bind_inherited_class(op_array, &op_array->opcodes[opline_num], EG(class_table), ce, 0);
    }
    opline_num = op_array->opcodes[opline_num].result.opline_num;
}

TL;DR

对于不扩展另一个 class.

的 classes,顺序无关紧要

任何正在扩展的 class 必须 在它实现之前定义(或者必须使用自动加载器)。