【问题标题】:How does a class extension or Interface work?类扩展或接口如何工作?
【发布时间】:2013-03-19 07:01:22
【问题描述】:

遇到过很多次,不知道为什么,这让我很好奇。一些类在声明之前就可以工作,而另一些则不能;

示例 1

$test = new TestClass(); // top of class
class TestClass {
    function __construct() {
        var_dump(__METHOD__);
    }
}

输出

 string 'TestClass::__construct' (length=22)

示例 2

当一个类扩展另一个类或实现任何接口时

$test = new TestClass(); // top of class
class TestClass implements JsonSerializable {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return json_encode(rand(1, 10));
    }
}

输出

Fatal error: Class 'TestClass' not found 

示例 3

让我们尝试上面相同的类,但改变位置

class TestClass implements JsonSerializable {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return json_encode(rand(1, 10));
    }
}

$test = new TestClass(); // move this from top to bottom 

输出

 string 'TestClass::__construct' (length=22)

示例 4(我也使用 class_exists 进行了测试)

var_dump(class_exists("TestClass")); //true
class TestClass {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return null;
    }
}

var_dump(class_exists("TestClass")); //true

一旦实现JsonSerializable(或任何其他)

var_dump(class_exists("TestClass")); //false
class TestClass implements JsonSerializable {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return null;
    }
}

var_dump(class_exists("TestClass")); //true

还检查了操作码withoutJsonSerializable

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   SEND_VAL                                                 'TestClass'
         1      DO_FCALL                                      1  $0      'class_exists'
         2      SEND_VAR_NO_REF                               6          $0
         3      DO_FCALL                                      1          'var_dump'
   4     4      NOP                                                      
  14     5    > RETURN                                                   1

还检查了操作码withJsonSerializable

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   SEND_VAL                                                 'TestClass'
         1      DO_FCALL                                      1  $0      'class_exists'
         2      SEND_VAR_NO_REF                               6          $0
         3      DO_FCALL                                      1          'var_dump'
   4     4      ZEND_DECLARE_CLASS                               $2      '%00testclass%2Fin%2FaDRGC0x7f563932f041', 'testclass'
         5      ZEND_ADD_INTERFACE                                       $2, 'JsonSerializable'
  13     6      ZEND_VERIFY_ABSTRACT_CLASS                               $2
  14     7    > RETURN                                                   1

问题

  • 我知道 Example 3 有效,因为该类是在其启动之前声明的,但为什么 Example 1 首先会有效?
  • 扩展或接口的整个过程如何在 PHP 中工作以使一个有效而另一个无效?
  • 示例 4 中究竟发生了什么?
  • Opcodes 应该让事情变得清晰,但只是让它变得更复杂,因为 class_existsTestClass 之前被调用,但情况正好相反。

【问题讨论】:

  • 我以前也想知道这个,我从Iterator和朋友那里知道的。
  • 如果实现的类也存在于同一个文件中,这会有所不同吗?也许这与 php 查找引用类的方式有关。 (查看文件,甚至可能访问 _autoload(),最后查看类的本机代码)
  • 即使包含一个类也是同样的问题......
  • 1) 它是无条件的,2) 接口必须在使用前声明,3) 以 JsonSerializeable 为条件,参见 2),4) DO_FCALL 需要 SEND_VAL 才能实际运行,考虑任何函数调用任何语言:函数(一、二、三)、一、二和三必须在 function() 发生之前有一个值。

标签: php class opcode


【解决方案1】:

我找不到关于 PHP 类定义的文章;但是,我想它与您的实验表明的User-defined functions 完全相同。

函数在被引用之前不需要被定义,除了当一个函数被有条件地定义时,如下面的两个例子所示。当以条件方式定义函数时;它的定义必须在被调用之前进行处理。

<?php

$makefoo = true;

/* We can't call foo() from here 
   since it doesn't exist yet,
   but we can call bar() */

bar();

if ($makefoo) {
  function foo()
  {
    echo "I don't exist until program execution reaches me.\n";
  }
}

/* Now we can safely call foo()
   since $makefoo evaluated to true */

if ($makefoo) foo();

function bar() 
{
  echo "I exist immediately upon program start.\n";
}

?>

对于类也是如此:

  • 示例 1 有效,因为该类不以其他任何条件为条件。
  • 示例 2 失败,因为该类以 JsonSerializable 为条件。
  • 示例 3 有效,因为该类在被调用之前已正确定义。
  • 示例 4 第一次为 false,因为该类是有条件的,但后来成功,因为该类已加载。

通过实现接口或从另一个文件扩展另一个类 (require),使该类成为有条件的。我称它为有条件的,因为该定义现在依赖于另一个定义。

想象一下 PHP 解释器首先查看这个文件中的代码。它看到一个非条件类和/或函数,所以它继续并将它们加载到内存中。它会看到一些有条件的并跳过它们。

然后解释器开始解析页面执行。在示例 4 中,它到达 class_exists("TestClass") 指令,检查内存,然后说不,我没有。如果没有它,因为它是有条件的。它继续执行指令,查看条件类并执行指令以将类实际加载到内存中。

然后它下降到最后一个class_exists("TestClass") 并看到该类确实存在于内存中。

在读取您的操作码时,TestClass 不会在 class_exist 之前被调用。您看到的是发送 value TestClass 的 SEND_VAL,以便它在内存中用于下一行,它实际上在 class_exists 上调用 DO_FCALL

然后您可以看到它如何处理类定义本身:

  1. ZEND_DECLARE_CLASS - 这是加载你的类定义
  2. ZEND_ADD_INTERFACE - 这会获取 JsonSerializable 并将其添加到您的类定义中
  3. ZEND_VERIFY_ABSTRACT_CLASS - 验证一切正常。

第二部分ZEND_ADD_INTERFACE似乎阻止了PHP引擎仅在其初始峰值加载类。

如果您希望更详细地讨论 PHP 解释器如何 在这些场景中编译和执行代码,我建议采取 看@StasManswer to this question,他 比这个答案更深入地提供了一个很好的概述。

我想我们回答了你所有的问题。

最佳实践:将每个类放在它自己的文件中,然后根据需要 autoload 它们,正如 @StasM 在他的回答中所说,使用合理的文件命名和自动加载策略 - 例如 PSR-0 或类似的东西。当您这样做时,您不再需要关心引擎加载它们的顺序,它只会自动为您处理。

【讨论】:

  • 我真的很喜欢你的回答——非常简洁,你提到了autoload。更多的人应该使用这个伟大的机制。
【解决方案2】:

基本前提是要使用的类必须被定义,即引擎知道。这永远无法改变——如果您需要某个类的对象,PHP 引擎需要知道该类是什么。

但是,引擎获得此类知识的时刻可能会有所不同。首先,引擎对 PHP 代码的使用包括两个独立的过程 - 编译和执行。在编译阶段,引擎将您知道的 PHP 代码转换为一组操作码(您已经熟悉),在第二阶段,引擎通过操作码,因为处理器将通过内存中的指令,并执行它们。

其中一个操作码是定义新类的操作码,通常插入到源中类定义所在的同一位置。

但是,当编译器遇到类定义时,它可能能够在执行任何代码之前将该类输入到引擎已知的类列表中。这称为“早期绑定”。如果编译器确定它已经拥有创建类定义所需的所有信息,并且没有理由将类的创建推迟到实际运行时,就会发生这种情况。目前,引擎仅在以下情况下执行此操作:

  1. 没有附加任何接口或特征
  2. 不是抽象的
  3. 要么不扩展任何类,要么只扩展引擎已知的类
  4. 被声明为顶级语句(即不在条件、函数等内部)

此行为也可以通过编译器选项进行修改,但这些选项仅适用于 APC 之类的扩展,因此除非您打算开发 APC 或类似的扩展,否则您不必太担心。

这也意味着这样就可以了:

 class B extends A {}
 class A { }

但这不会是:

 class C extends B {}
 class B extends A {}
 class A { }

由于 A 是早期绑定的,因此可用于 B 的定义,但 B 将仅在第 2 行中定义,因此无法用于第 1 行的 C 定义。

在您的情况下,当您的类实现接口时,它不是早期绑定的,因此在到达“类”语句时引擎就知道了。当它是没有接口的简单类时,它是早期绑定的,因此一旦文件编译完成就被引擎知道(您可以将这一点视为文件中第一个语句之前的一个)。

为了不打扰引擎的所有这些奇怪的细节,我会支持上一个答案的建议 - 如果您的脚本很小,只需在使用前声明类。如果您有更大的应用程序,请在单个文件中定义您的类,并具有合理的文件命名和自动加载策略 - 例如PSR-0 或类似的,适合您的情况。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-21
    • 2012-09-01
    • 2019-01-06
    • 1970-01-01
    相关资源
    最近更新 更多