42

我对 PHP 很陌生,但多年来我一直在用类似的语言进行编程。我被以下情况弄糊涂了:

class Foo {
    public $path = array(
        realpath(".")
    );
}

它产生了一个语法错误:Parse error: syntax error, unexpected '(', expecting ')' in test.php on line 5这是realpath调用。

但这很好用:

$path = array(
    realpath(".")
);

在对此进行了一段时间的抨击之后,我被告知您不能在属性默认值中调用函数;你必须在__construct. 我的问题是:为什么?!这是一个“功能”还是草率的实现?理由是什么?

4

5 回答 5

54

编译器代码表明这是设计使然,尽管我不知道其背后的官方推理是什么。我也不确定要可靠地实现此功能需要付出多少努力,但目前的完成方式肯定存在一些限制。

尽管我对 PHP 编译器的了解并不广泛,但我将尝试说明我认为发生的事情,以便您了解哪里存在问题。您的代码示例非常适合此过程,因此我们将使用它:

class Foo {
    public $path = array(
        realpath(".")
    );
}

如您所知,这会导致语法错误。这是PHP 语法的结果,它做出以下相关定义:

class_variable_declaration: 
      //...
      | T_VARIABLE '=' static_scalar //...
;

因此,在定义诸如 等变量的值时$path,期望值必须与静态标量的定义相匹配。不出所料,这有点用词不当,因为静态标量的定义还包括其值也是静态标量的数组类型:

static_scalar: /* compile-time evaluated scalars */
      //...
      | T_ARRAY '(' static_array_pair_list ')' // ...
      //...
;

让我们假设语法不同,并且类变量声明规则中的注释行看起来更像以下内容,它将与您的代码示例匹配(尽管破坏了其他有效的分配):

class_variable_declaration: 
      //...
      | T_VARIABLE '=' T_ARRAY '(' array_pair_list ')' // ...
;

重新编译 PHP 后,示例脚本将不再因该语法错误而失败。相反,它会因编译时错误"Invalid binding type"而失败。由于代码现在基于语法是有效的,这表明实际上在编译器的设计中存在某些特定的东西导致了问题。为了弄清楚那是什么,让我们暂时回到原始语法,并假设代码示例有一个有效的$path = array( 2 );.

使用语法作为指导,可以在解析此代码示例时遍历编译器代码中调用的操作。我遗漏了一些不太重要的部分,但过程看起来像这样:

// ...
// Begins the class declaration
zend_do_begin_class_declaration(znode, "Foo", znode);
    // Set some modifiers on the current znode...
    // ...
    // Create the array
    array_init(znode);
    // Add the value we specified
    zend_do_add_static_array_element(znode, NULL, 2);
    // Declare the property as a member of the class
    zend_do_declare_property('$path', znode);
// End the class declaration
zend_do_end_class_declaration(znode, "Foo");
// ...
zend_do_early_binding();
// ...
zend_do_end_compilation();

尽管编译器在这些不同的方法中做了很多工作,但重要的是要注意一些事情。

  1. 调用zend_do_begin_class_declaration()导致调用get_next_op()。这意味着它将一个新的操作码添加到当前操作码数组中。
  2. array_init()并且zend_do_add_static_array_element()不生成新的操作码。相反,该数组会立即创建并添加到当前类的属性表中。方法声明以类似的方式工作,通过zend_do_begin_function_declaration().
  3. zend_do_early_binding() 使用当前操作码数组上的最后一个操作码,在将其设置为 NOP 之前检查以下类型之一:
    • ZEND_DECLARE_FUNCTION
    • ZEND_DECLARE_CLASS
    • ZEND_DECLARE_INHERITED_CLASS
    • ZEND_VERIFY_ABSTRACT_CLASS
    • ZEND_ADD_INTERFACE

请注意,在最后一种情况下,如果操作码类型不是预期类型之一,则会引发错误—— “无效绑定类型”错误。由此,我们可以看出,允许以某种方式分配非静态值会导致最后一个操作码与预期不同。那么,当我们使用带有修改语法的非静态数组时会发生什么?

array_init()编译器不是调用,而是准备参数和调用zend_do_init_array()。这反过来又调用get_next_op()并添加了一个新的INIT_ARRAY 操作码,产生如下内容:

DECLARE_CLASS   'Foo'
SEND_VAL        '.'
DO_FCALL        'realpath'
INIT_ARRAY

这就是问题的根源。通过添加这些操作码,zend_do_early_binding()获取意外输入并引发异常。由于早期绑定类和函数定义的过程似乎是 PHP 编译过程不可或缺的一部分,因此不能忽略它(尽管 DECLARE_CLASS 生产/消费有点混乱)。同样,尝试并内联评估这些额外的操作码是不切实际的(您不能确定给定的函数或类是否已被解析),因此无法避免生成操作码。

一个潜在的解决方案是构建一个新的操作码数组,该数组的范围为类变量声明,类似于方法定义的处理方式。这样做的问题是决定何时评估这种一次性序列。是否会在加载包含该类的文件时、首次访问属性时或构造该类型的对象时完成?

正如您所指出的,其他动态语言已经找到了处理这种情况的方法,因此做出该决定并使其发挥作用并非不可能。但据我所知,在 PHP 的情况下这样做不会是单行修复,而且语言设计者似乎已经决定在这一点上它不值得包括在内。

于 2010-10-22T20:04:38.683 回答
23

我的问题是:为什么?!这是一个“功能”还是草率的实现?

我会说这绝对是一个功能。类定义是代码蓝图,在定义时不应该执行代码。它会破坏对象的抽象和封装。

然而,这只是我的看法。我不能确定开发人员在定义它时有什么想法。

于 2010-10-18T14:50:37.630 回答
7

您可能可以实现类似的效果:

class Foo
{
    public $path = __DIR__;
}

IIRC__DIR__需要 php 5.3+,__FILE__已经存在了更长的时间

于 2010-10-18T14:57:18.750 回答
5

这是一个草率的解析器实现。我没有正确的术语来描述它(我认为术语“beta 减少”以某种方式适合......),但是 PHP 语言解析器比它需要的更复杂,更复杂,所以各种各样不同的语言结构需要特殊大小写。

于 2010-10-18T14:51:36.397 回答
2

我的猜测是,如果错误没有发生在可执行行上,您将无法获得正确的堆栈跟踪......因为用常量初始化值不会有任何错误,所以没有问题,但是函数可以抛出异常/错误,并且需要在可执行行中调用,而不是声明性行。

于 2010-10-18T14:52:46.440 回答