【问题标题】:Why don't PHP attributes allow functions?为什么 PHP 属性不允许函数?
【发布时间】:2011-04-26 23:54:15
【问题描述】:

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

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

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

但这很好用:

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

在我的头撞了一会儿之后,我被告知你不能在属性默认值中调用函数;您必须在__construct 中进行操作。我的问题是:为什么?!这是一个“功能”还是草率的实现?理由是什么?

【问题讨论】:

  • @Gordon 请改进答案。
  • @Schwern 请改进问题。
  • 明确地说,当问题得到明确回答时,我接受。到目前为止,所有的答案都是猜测。有帮助,但仍然是猜测。
  • @Schwern 好吧,您可以看看the source code 并自己判断它是草率还是功能(或两者兼而有之)。我想它在zend_object.c 中,但我对 Zend 引擎不太熟悉,所以您可能需要挖掘一下。我将 zend-engine 添加到标签列表中。也许它会吸引更多知识渊博的人。
  • 我很喜欢谷歌搜索“请改进答案”时再次找到这个问题的方式:)

标签: php language-design php-internals


【解决方案1】:

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

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

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

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

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 后,示例脚本将不再因语法错误而失败。相反,它会因编译时错误“无效的绑定类型”而失败。由于代码现在基于语法是有效的,这表明实际上在编译器的设计中存在某些特定的东西导致了问题。为了弄清楚那是什么,让我们暂时回到原来的语法,假设代码示例有一个有效的 $path = array( 2 ); 赋值。

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

// ...
// 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 opcode,产生如下内容:

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

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

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

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

【讨论】:

  • 谢谢!何时评估的答案指出了 PHP 属性默认语法中的明显缺陷:您根本不应该分配给它,它应该在对象构造函数中设置。歧义已解决。 (对象是否尝试共享该常量?) 至于静态属性,没有歧义,它们可以被允许任何表达式。 Ruby 就是这样做的。我怀疑他们没有删除对象属性默认值,因为缺少类构造函数,没有设置类属性的好方法。而且他们不希望对对象和类属性默认值有单独的限制。
  • @Schwern:乐于助人!这是我过去一直很好奇但从未想过要详细检查的事情,所以这是一个了解到底发生了什么的好机会。关于赋值,允许这种赋值避免了如果你不“需要”一个构造函数就强迫你创建一个构造函数……我觉得这将是一个可怕的理由,尽管在 PHP 的情况下,不是一个令人震惊的.我认为每个实例都会在创建时复制默认属性值,但我可能弄错了,所以它们可能会尝试共享。
  • 无论如何,这样做所获得的节省(考虑到您首先可以分配的有限数据)将是最小的,所以我不确定是否值得进行此设置。至于你的 cmets 关于解决歧义的问题,我倾向于同意。
  • SO 上一定有 PHP 核心开发者。还有谁会对这个答案给出-1?
【解决方案2】:

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

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

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

【讨论】:

  • +1 我同意,例如,如果我说:public $foo = mktime() 它会节省从类被解析、构造或尝试访问静态类时的时间。
  • 如前所述,没有定义表达式何时被计算。但是,您应该能够为属性分配一个闭包——它可以毫无歧义地返回时间——但这也会产生语法错误。
  • 所以它是一种 BDSM 语言设计,在其他方面非常宽松的语言中,并作为语法错误实现?
  • 抱歉,我试图编辑它以减少争论,但时间已用完。我想说的方式:我想看到一个关于这个理由的引用。这种级别的 BDSM 在动态语言中似乎非常不合适,尤其是在 PHP 中。此外,在定义时执行代码如何破坏抽象或封装?类定义不必每次运行都完全相同。
  • @Hannes 这就像把厨房里所有的刀和炉子都搬走,这样厨师就不会割伤自己或被烫伤。它非常安全,但你不能做太多的烹饪。相信你的厨师不会是彻头彻尾的白痴。
【解决方案3】:

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

class Foo
{
    public $path = __DIR__;
}

IIRC __DIR__ 需要 php 5.3+,__FILE__ 存在的时间更长

【讨论】:

  • 好点。这是有效的,因为它是一个神奇的常数,将在解析时被替换
  • 谢谢,但示例仅用于说明。
【解决方案4】:

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

【讨论】:

  • 其他语言允许吗?我很好奇,因为我真的不知道。如果我没记错的话,Pascal/Delphi 没有。
  • @Pekka:静态语言通常不会,因为它们中的类几乎总是只是一个编译器构造。但是对于动态语言,类是在定义执行时创建的,所以没有理由不能使用当时函数的返回值作为属性的值。
  • @Ignacio 欢呼。好吧,那是真的。总的来说,我仍然认为这是一件好事,因为它执行了良好的 OOP 原则。
  • @pekka Perl 6 可以做到这一点,这里 (dl.dropbox.com/u/7459288/Perl%206%20Examples/Person.p6) 是一个例子。
  • 是的,其他动态语言允许这样做。 Ruby、Perl 5(通过多种方式)、Perl 6 和 Python(我很确定)。要么 PHP 语言设计者受到打击并认为他们正在编程 Java,要么这是实现的限制。
【解决方案5】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-27
    • 2014-10-17
    • 2013-03-03
    • 2017-05-27
    • 2017-02-21
    • 1970-01-01
    相关资源
    最近更新 更多