类扩展或接口是如何工作的?

33

我遇到过这种情况很多次,但不确定为什么,所以让我感到好奇。有些类在声明之前就可以使用,而有些则不能;

例子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

还检查了没有 JsonSerializable 的操作码

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

同时检查具有 JsonSerializable 的操作码。

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本来应该使事情更清晰,但由于在调用TestClass之前调用了class_exists,反过来了。

问题

  • 为什么Example 1最初就能够工作?
  • PHP中的扩展或接口继承如何工作,使得其中一个有效而另一个无效?
  • 在示例4中到底发生了什么?
  • Opcodes事实上使问题更加复杂,因为class_exists在调用TestClass之前被调用,而不是相反。

2
жҲ‘иҝҮеҺ»д№ҹжӣҫз»ҸжғіиҝҮиҝҷдёӘй—®йўҳпјҢжҲ‘д»ҺIteratorе’Ңзӣёе…ізҡ„зұ»дёӯдәҶи§ЈеҲ°дәҶиҝҷдёҖзӮ№гҖӮ - hakre
如果实现的类也在同一个文件中,这会有所不同吗?也许这与PHP查找引用类的方式有关。(在文件中查找,甚至访问_autoload(),最后在本地代码中查找类) - nl-x
1
即使包含一个类,问题仍然是相同的... - Baba
1
  1. 它是无条件的。
  2. 在使用之前必须声明接口。
  3. 取决于JsonSerializeable,请参见2)。
  4. DO_FCALL需要一个SEND_VAL才能实际运行,考虑任何语言中的任何函数调用:function(one, two, three),在function()发生之前,one、two和three必须有一个值。
- Joe Watkins
2个回答

18

我找不到有关PHP类定义的文章,但我想它与您的实验所示的用户定义函数完全相同。

函数在引用之前无需定义,除非函数是按条件定义的,如下面两个示例所示。当以条件方式定义函数时,必须在调用之前处理其定义

<?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"),并查看内存中确实存在该类。阅读您的操作码时,TestClassclass_exist之前没有被调用。您看到的是SEND_VAL,它正在发送值TestClass,以便下一行将其放入内存中,后者实际上调用了class_exists。然后,您可以看到它如何处理类定义本身:1. ZEND_DECLARE_CLASS - 这将加载您的类定义。2. ZEND_ADD_INTERFACE - 这会获取JsonSerializable并将其添加到类定义中。3.ZEND_VERIFY_ABSTRACT_CLASS - 这会验证所有内容是否合理。正是第二个部分ZEND_ADD_INTERFACE似乎防止PHP引擎仅在初始查看时加载类。
如果您希望更详细地讨论PHP解释器在这些情况下如何编译和执行代码,我建议查看@StasM此问题的答案中提供的优秀概述,他比本答案更深入地介绍了它。
我认为我们回答了你所有的问题。
最佳实践:将每个类放在自己的文件中,然后按需autoload它们,如@StasM在他的答案中所述,使用合理的文件命名和自动加载策略 - 例如PSR-0或类似的东西。当您这样做时,您就不再需要担心引擎加载它们的顺序,它会自动处理这一点。

1
我非常喜欢你的回答 - 非常简洁,而且你提到了“autoload”。更多的人应该使用这个伟大的机制。 - Joshua

5
基本前提是,为了使用类,必须先定义好它,也就是PHP引擎要知道这个类的存在。这是无法改变的,如果需要使用某个类的对象,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的定义。

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

为了避免麻烦引擎的所有这些奇怪细节,我支持以前的答案建议 - 如果您的脚本很小,只需在使用之前声明类。如果您有更大的应用程序,请在单独的文件中定义您的类,并具有明智的文件命名和自动加载策略 - 例如PSR-0或类似的东西,适合您的情况。


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