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

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中执行。我的问题是:为什么?! 这是一种“特性”还是不良实现?其背后的原理是什么?


1
@Schwern 嗯,你可以看一下源代码并自行判断它是粗糙还是特性(或两者兼有)。我猜它在zend_object.c中,但我对Zend引擎不是很熟悉,所以你可能需要挖掘一下。我已经将Zend引擎添加到标签列表中。也许会吸引更多了解的人。 - Gordon
参考自http://phpsadness.com/sad/37。 - Solomon Ucko
PHP有一个叫做attributes的特性,但这不是它们。这是一个属性初始化。 - mdfst13
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()得到了意外的输入,从而抛出了异常。

早期绑定类和函数定义的过程似乎非常重要,因此不能忽略它(尽管DECLARE_CLASS的生产/消费有点混乱)。同样,尝试在内联中评估这些附加操作码也不切实际(无法确定给定的函数或类是否已解析),因此无法避免生成操作码。

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

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


谢谢!关于何时评估的答案指出了PHP属性默认语法的明显缺陷:您根本不应该能够对其进行分配,它应该在对象构造函数中设置。歧义得到解决。(对象是否尝试共享该常量?)至于静态属性,没有歧义,它们可以允许任何表达式。这就是Ruby的做法。我怀疑他们没有删除对象属性默认值,因为缺乏类构造函数,没有很好的方法来设置类属性。而且他们不想为对象和类属性默认值制定单独的规定。 - Schwern
@Schwern: 很高兴能帮忙!这是我过去一直很好奇但从未想到要详细了解的事情,所以这是一个很好的机会来弄清楚究竟发生了什么。关于赋值,允许这种类型的赋值避免了在不需要“创建”构造函数时被强制创建...虽然在PHP的情况下,我觉得这将是一个可怕的理由,但也不是一个令人震惊的理由。我认为每个实例在创建时都会复制默认属性值,但我可能错了,所以它们可能会尝试共享。 - Tim Stone
无论如何,这样做所获得的节省(考虑到你可以在第一次分配时分配的有限数据)将是微不足道的,因此我不确定是否值得拥有这种设置。至于您关于消除歧义的评论,我倾向于同意。 - Tim Stone
9
这里在SO上肯定有PHP核心开发者。否则谁会给这个回答点一个-1呢? - Boldewyn

23
我的问题是:为什么?!这是一种“特性”还是粗糙的实现?
我认为这绝对是一种特性。类定义是代码蓝图,不应在定义时执行代码。这会破坏对象的抽象和封装。
然而,这只是我的看法。我无法确定开发人员在定义此内容时的想法。

6
我同意。例如,如果我说:public $foo = mktime(),它会保存类被解析、构造或尝试访问静态成员的时间? - Hannes
1
如上所述,表达式的求值时间未定义。但是,您应该能够将闭包分配给属性 - 这可以消除歧义地返回时间 - 但这也会产生语法错误。 - erisco
7
所以说,这是在一个非常开明的语言中进行 BDSM 风格设计并作为一种语法错误来实现的? - Schwern
2
抱歉,我试图修改它以减少争议,但时间不够了。我想说的是:我想看到那个理论的引用。那种程度的BDSM在动态语言中,特别是在PHP中似乎完全不合适。此外,在定义时执行代码如何破坏抽象或封装?每次运行时类定义并不一定完全相同。 - Schwern
3
@Hannes 这就像是把厨房里所有的刀子和炉灶都拿走,以便没有任何厨师割伤或烧伤自己。这样很安全,但是你也很难做出更多的菜肴。相信你的厨师不会太过愚蠢。 - Schwern
显示剩余7条评论

7
您可能可以实现类似如下的效果:
class Foo
{
    public $path = __DIR__;
}

据我所知,__DIR__ 需要 PHP 5.3+,而 __FILE__ 已经存在更久了。


好的观点。这是有效的,因为它是一个魔术常量,并将在解析时被替换。 - Pekka
4
谢谢你,但这个例子只是为了说明。 - Schwern

5

这是一个粗糙的解析器实现。我没有正确的术语来描述它(我认为术语“beta减少”在某种程度上适合...),但PHP语言解析器比它需要的更复杂和更复杂,因此需要对不同的语言结构进行各种特殊处理。


其他编程语言允许这样吗?我很好奇,因为我不确定。如果我没记错的话,Pascal/Delphi 不允许。 - Pekka
2
@Pekka:静态语言通常不会,因为它们中的类几乎总是只是编译器构造。但是对于动态语言,当执行定义时创建类,因此没有理由不能在那时使用函数的返回值作为属性的值。 - Ignacio Vazquez-Abrams
@Ignacio 你好。没错,我也认同这一点。总体来说,我仍然认为这是一件好事,因为它强制实施了良好的面向对象编程原则。 - Pekka
@pekka Perl 6可以做到这一点,这里(http://dl.dropbox.com/u/7459288/Perl%206%20Examples/Person.p6)有一个例子。 - mfollett
1
是的,其他动态语言允许这样做。Ruby、Perl 5(通过多种方式)、Perl 6和Python(我非常确定)。要么PHP语言设计者被打了一顿,以为自己在编写Java程序,要么这是一种实现限制。 - Schwern

2

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


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