为什么不能从尚未定义的类继承,该类又继承自尚未定义的类?

28

我研究有关类编译、它们的顺序和逻辑。

如果我在一个简单的父类之前声明一个类:

 class First extends Second{}
 class Second{}

这将可以正常工作。 查看跨PHP版本的实时示例。

但是如果父类还有一些未声明的父类(extends或implements),例如此示例:

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

我会遇到一个错误:

致命错误:找不到“Second”类...

在不同PHP版本下查看实际示例。

那么为什么在第二个示例中无法找到Second类呢? 也许PHP不能编译该类,因为它还需要编译Third类,或者是其他原因?

我正在尝试弄清楚为什么在第一个示例中,PHP会编译Second类,但如果它有一些父类,则不会。我做了很多研究,但没有确切的答案。

  • 我不是要以这种方式编写代码,但在这个示例中,我试图理解编译及其顺序的工作原理。

3
你的理解是错误的,Second 应该扩展 FirstThird 则应该扩展 Second。至少在通常情况下是这样做的。 - Sverri M. Olsen
5
为什么投票关闭这个问题?我已经做了一些研究,但没有找到明确的答案。我认为应该有一个确切的答案。 - sergio
4
我认为这是一个有趣的问题。实际上,这可能与PHP解析依赖项的方式有关,但由于在PHP 4、5、7和HHVM中保持一致,所以它很可能是引擎中更根本性的东西,而不是实现细节。(参见http://3v4l.org/9WJFq与http://3v4l.org/ZCVWQ) - IMSoP
1个回答

34

因此,PHP使用了所谓的“延迟绑定”。基本上,继承和类定义直到文件编译结束才发生。

这其中有许多原因。第一个原因是你所示例的:(first extends second {} 起作用)。第二个原因是 opcache。

为了使编译在 opcache 的领域内正确工作,编译必须在没有其他已编译文件状态的情况下进行。这意味着,在编译文件时,类符号表被清空。

然后,将缓存该编译结果。然后在运行时,当从内存加载已编译文件时,opcache 运行延迟绑定,然后进行继承并实际声明类。

class First {}
当那个类被看到时,它立即被添加到符号表中。不管它在文件中的位置在哪里。因为没有延迟绑定任何东西,它已经完全定义了。这种技术称为“早期绑定”,它允许您在声明之前使用类或函数。
class Third extends Second {}

当这个变量被看到时,它已经被编译了,但实际上还没有被声明。相反,它会被添加到一个“延迟绑定”的列表中。

class Second extends First {}

当最终看到这个时,它也被编译了,而不是实际声明。它被添加到后期绑定列表中,但在 Third 后面。

因此,现在当进行后期绑定过程时,它会逐一检查“后期绑定”的类列表。它首先看到的是 Third。然后尝试找到 Second 类,但找不到(因为它实际上还没有被声明)。所以会抛出错误。

如果重新排列这些类:

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。因此,我们可以相应地调整编译过程。

但是,当我们将opcode缓存加入其中时,事情变得更加复杂。如果我们使用b1.php的全局状态来编译Foo,然后在以后(在不同的请求中)切换到b2.php,事情会以奇怪的方式崩溃。

因此,opcode缓存在编译文件之前将全局状态置为空。因此,a.php将被编译为应用程序中唯一的文件。

编译完成后,它将被缓存在内存中(以便供以后的请求重复使用)。

然后,在那一点之后(或在以后的请求中从内存中加载之后),"延迟"步骤发生。这样,编译的文件就与请求的状态耦合在一起。

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

源代码。

为了理解原因,让我们看看源代码。

Zend/zend_compile.c中,我们可以看到编译类的函数: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;
}

它最初会发出一个操作码来声明继承类。编译完成后,将调用名为zend_do_early_binding()的函数。该函数预先声明文件中的函数和类(因此它们在顶部可用)。对于普通类和函数,它只是将它们添加到符号表中(声明它们)。

有趣的部分是继承的情况:

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语句主要是试图从符号表中获取类并检查是否不存在。第二个if语句检查我们是否正在使用延迟绑定(启用了opcache)。

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

最后,通常由opcache调用函数zend_do_delayed_early_binding(),该函数遍历列表并实际绑定继承的类:

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;
}

简而言之

对于没有继承其他类的类,顺序并不重要。

任何被继承的类,在实现之前必须先定义(或者使用自动加载器)。


由于它似乎是有效的答案,我们需要删除其他不正确的答案和评论。我真的想要这个的参考和工作原理,如果您可以在此处添加一个链接(PHP中的后期绑定,opcache),那将更好。 - A.B
@ircmaxell 为什么要使用这些后期绑定来实现接口和扩展类?有什么优势吗?或者这对于这个 SO 线程来说太多了吗? - Xatenev
1
@ircmaxell,非常好的、开放广阔的回答,这正是我在寻找的。 - sergio
@Xatenev 在那方面添加了一节。 - ircmaxell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接