我是PHP的新手,但我已经用类似语言编程多年了。我对以下内容感到困惑:
class Foo {
public $path = array(
realpath(".")
);
}
它产生了语法错误:Parse error: syntax error, unexpected '(', expecting ')' in test.php on line 5
这是realpath
调用。
但这很好用:
$path = array(
realpath(".")
);
在我的头撞了一会儿之后,我被告知你不能在属性默认情况下调用函数;你必须在__construct
中完成。我的问题是:为什么?!这是“功能”还是草率实施?理由是什么?
答案 0 :(得分:51)
编译器代码表明这是设计的,尽管我不知道背后的官方推理是什么。我也不确定可靠地实现这个功能需要多少努力,但是目前的工作方式肯定存在一些限制。
虽然我对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();
虽然编译器在这些方法中做了很多,但重要的是要注意一些事情。
zend_do_begin_class_declaration()
会致电get_next_op()
。这意味着它会向当前的操作码数组添加一个新的操作码。array_init()
和zend_do_add_static_array_element()
不会生成新的操作码。而是立即创建数组并将其添加到当前类的属性表中。方法声明以类似的方式工作,通过zend_do_begin_function_declaration()
中的特殊情况。zend_do_early_binding()
使用当前操作码数组上的最后一个操作码,在将其设置为NOP之前检查以下类型之一:
请注意,在最后一种情况下,如果操作码类型不是预期类型之一,则会引发错误 - “无效绑定类型”错误。由此可以看出,允许以某种方式分配非静态值会导致最后一个操作码不是预期的。那么,当我们使用带有修改过的语法的非静态数组时会发生什么?
编译器不是调用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的情况下这样做不会是单行修复,语言设计者似乎已经决定在这一点上不值得包括。
答案 1 :(得分:20)
我的问题是:为什么?!这是“功能”还是草率实施?
我说这绝对是一个特色。类定义是代码蓝图,不应该在定义时执行代码。它会破坏对象的抽象和封装。
然而,这只是我的看法。我无法确定开发人员在定义时有什么想法。
答案 2 :(得分:6)
你可能会达到类似的目的:
class Foo
{
public $path = __DIR__;
}
IIRC __DIR__
需要php 5.3 +,__FILE__
需要更长时间
答案 3 :(得分:5)
这是一个草率的解析器实现。我没有正确的术语来描述它(我认为术语“beta减少”在某种程度上适合...),但是PHP语言解析器比它需要的更复杂和更复杂,所以各种各样的不同的语言结构需要特殊的外壳。
答案 4 :(得分:2)
我的猜测是,如果在可执行行上没有发生错误,你将无法获得正确的堆栈跟踪...因为初始化值与常量不会有任何错误,所以没有问题使用它,但函数可以抛出异常/错误,需要在可执行行中调用,而不是声明性的。