【问题标题】:Why can't you inherit from a not-yet-defined class which inherits from a not-yet-defined class?为什么不能从继承自尚未定义的类的尚未定义的类继承?
【发布时间】:2015-06-20 03:52:08
【问题描述】:

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

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

 class First extends Second{}
 class Second{}

这可以正常工作。 See live example across PHP versions.

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

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

我会报错:

致命错误:未找到“第二”类...

See live example across PHP versions.

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

我试图找出为什么在第一个示例中,PHP 编译类 Second,但如果它有一些父类,它不会。我研究了很多,但没有什么确切的。

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

【问题讨论】:

  • 你搞错了。 Second 应该扩展 FirstThird 应该扩展 Second。至少,通常情况下是这样的。
  • 为什么投票结束这个问题?我已经对此进行了研究,但不清楚。我认为应该有确切的答案
  • 我认为这是一个有趣的问题,实际上。它可能与 PHP 解决依赖关系的方式有关,但鉴于它在 PHP 4、5、7 和 HHVM 中是一致的,它可能比引擎中的实现细节更基本。 (参见3v4l.org/9WJFq3v4l.org/ZCVWQ

标签: php inheritance php-internals


【解决方案1】:

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

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

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

编译完成后缓存到内存中(供后续请求重用)。

然后,在那之后(或在将来的请求中从内存中加载之后),“延迟”步骤就会发生。然后将编译后的文件与请求的状态耦合。

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

源代码。

要了解原因,让我们看一下源代码。

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)。

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

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

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

对于不扩展另一个类的类,顺序无关紧要。

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

【讨论】:

  • 由于它似乎是有效的答案,我们需要删除其他不正确的 asnwers 和 cmets,我真的很想对此进行参考,如果您可以在此添加一个链接会更好(后期绑定在 php, opcache)
  • @ircmaxell 为什么那些后期绑定用于实现接口和扩展类?有什么好处?还是这个 SO 线程太多了?
  • @ircmaxell,很好的,开放的答案,这就是我想要的。
  • @Xatenev 对此添加了一节。
猜你喜欢
  • 2021-12-12
  • 2015-09-12
  • 2013-10-19
  • 1970-01-01
  • 1970-01-01
  • 2011-01-28
  • 1970-01-01
  • 2022-09-24
  • 2012-06-07
相关资源
最近更新 更多