为什么你不能从一个尚未定义的 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 必须 在它实现之前定义(或者必须使用自动加载器)。
我研究 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 必须 在它实现之前定义(或者必须使用自动加载器)。