为什么PHP属性不允许函数?

Sch*_*ern 41 php language-design php-internals

我是PHP的新手,但我已经用类似语言编程多年了.我对以下内容感到困惑:

class Foo {
    public $path = array(
        realpath(".")
    );
}
Run Code Online (Sandbox Code Playgroud)

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

但这很好用:

$path = array(
    realpath(".")
);
Run Code Online (Sandbox Code Playgroud)

在我的头撞了一会儿之后,我被告知你不能在属性默认情况下调用函数; 你必须这样做__construct.我的问题是:为什么?!这是"功能"还是草率实施?理由是什么?

Tim*_*one 51

编译器代码表明这是设计的,虽然我不知道背后的官方推理是什么.我也不确定可靠地实现这个功能需要多少努力,但是目前的工作方式肯定存在一些限制.

虽然我对PHP编译器的了解并不广泛,但我会尝试说明我相信的内容,以便您可以看到存在问题的位置.您的代码示例是此过程的良好候选者,因此我们将使用它:

class Foo {
    public $path = array(
        realpath(".")
    );
}
Run Code Online (Sandbox Code Playgroud)

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

class_variable_declaration: 
      //...
      | T_VARIABLE '=' static_scalar //...
;
Run Code Online (Sandbox Code Playgroud)

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

static_scalar: /* compile-time evaluated scalars */
      //...
      | T_ARRAY '(' static_array_pair_list ')' // ...
      //...
;
Run Code Online (Sandbox Code Playgroud)

让我们假设语法是不同的,并且类变量说明规则中的注释行看起来更像下面的代码样本(尽管打破了其他有效的赋值):

class_variable_declaration: 
      //...
      | T_VARIABLE '=' T_ARRAY '(' array_pair_list ')' // ...
;
Run Code Online (Sandbox Code Playgroud)

重新编译PHP后,示例脚本将不再因语法错误而失败.相反,它会因编译时错误"无效的绑定类型"而失败.由于代码现在基于语法有效,这表明在编译器的设计中确实存在某些特定的东西,这会引起麻烦.为了弄清楚是什么,让我们暂时恢复原始语法,并想象代码示例有一个有效的赋值$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();
Run Code Online (Sandbox Code Playgroud)

虽然编译器在这些不同的方法中做了很多,但重要的是要注意一些事情.

  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
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 哇,一个很好的答案.+1 (8认同)
  • 在这里必须有一个PHP核心开发人员.还有谁会对这个答案给出一个-1? (8认同)

Pek*_*ica 21

我的问题是:为什么?!这是"功能"还是草率实施?

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

但是,这只是我的看法.我无法确定开发人员在定义时有什么想法.

  • 所以它在一种非常宽松的语言中设计了一些BDSM语言并实现为语法错误? (7认同)
  • +1我同意,例如,如果我说:`public $ foo = mktime()`它将节省从解析,构造类或尝试访问静态时的时间. (5认同)
  • @Hannes这就像从厨房里取出所有刀具和炉子一样,所以没有厨师自己割伤或烧焦.它非常安全,但你不能做太多的烹饪.相信你的厨师不要完全白痴. (3认同)
  • 对不起,我试着把它编辑成不那么有争议但是没时间了.我想说的方式:我希望看到这个理由的引用.在动态语言和特别是PHP中,BDSM的这种程度似乎非常不合适.此外,在定义时执行代码如何破坏抽象或封装?每次运行时,类定义不必完全相同. (2认同)

Rob*_*bin 6

你可以实现类似这样的东西:

class Foo
{
    public $path = __DIR__;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 谢谢,但这个例子只是为了说明. (4认同)

Ign*_*ams 5

这是一个草率的解析器实现.我没有正确的术语来描述它(我认为术语"beta减少"在某种程度上适合...),但PHP语言解析器比它需要的更复杂和更复杂,所以各种各样的不同的语言结构需要特殊的外壳.

  • @pekka那些"优秀的OOP原则"是什么? (3认同)
  • @Pekka:静态语言通常不会,因为它们中的类几乎总是只是一个编译器构造.但是对于动态语言,在执行定义时会创建类,因此没有理由不能将当时函数的返回值用作属性的值. (2认同)