括号改变函数调用结果的语义

52

另一个问题中指出,将PHP函数调用的结果用括号括起来可以将其转换为完整的表达式,从而使以下内容正常工作:

<?php
error_reporting(E_ALL | E_STRICT);

function get_array() {
   return array();
}

function foo() {
   // return reset(get_array());
   //              ^ error: "Only variables should be passed by reference"

   return reset((get_array()));
   //           ^ OK
}

foo();

我正在尝试在文档中找到任何明确和无歧义地解释这里正在发生什么的内容。与C++不同,我对PHP语法及其对语句/表达式的处理了解不足,无法自己推导出来。

文档中是否隐藏有关于此行为的信息?如果没有,是否有其他人可以不借助假设来解释它?


更新

我最初发现这个EBNF试图表示PHP语法,并尝试自己解码我的脚本,但最终放弃了。

然后,使用phc生成两个foo()变体的.dot文件,我使用以下命令为两个脚本生成AST图像:

$ yum install phc graphviz
$ phc --dump-ast-dot test1.php > test1.dot
$ dot -Tpng test1.dot > test1.png
$ phc --dump-ast-dot test2.php > test2.dot
$ dot -Tpng test2.dot > test2.png

在这两种情况下,结果完全相同:

Parse tree of snippets 1 and 2


1
看起来这只适用于以单个函数调用形式表达的表达式。 - hakre
3
Array() 用大写字母 A?据我所知,语言结构应该写作 array() - knittl
7
PHP因此不区分大小写。 - Wrikken
2
只有一个函数调用能够使用this关键字的原因是,只有变量或者单一返回引用的函数才可以作为“reset”的正确输入。变量显然总是通过引用来工作,这就让我们只能使用函数调用。但由于可能存在像$variablewithafunctionname()这样的情况,所以只有在执行时才会检查函数调用是否满足条件。如果()使reset不报错...那么在reset获取输入时,它就是一个引用(refcount > 1),这意味着(get_array())表达式在内存中留下了一些zval。 - Wrikken
1
深入挖掘,严格警告是来自VM部分/运行时。致命错误(不在Q的示例中,一个可能是:return reset((get_array()?:0));)已经在编译时出现,措辞更加严厉:“致命错误:只有变量可以通过引用传递”(如果函数返回引用,则一切都很好)。在发出严格通知之前会检查许多标志,我嗅到了其中的某些东西,但我对PHP内部不太了解:php-trunk/Zend/zend_vm_execute.h line 10853~ - hakre
显示剩余11条评论
2个回答

32

这种行为可以被归类为错误,因此您绝对不应该依赖它。

使消息在函数调用时被抛出的(简化的)条件如下(请参见定义操作码ZEND_SEND_VAR_NO_REF):

  • 参数不是函数调用(或者如果是,则通过引用返回),并且
  • 参数是一个引用或者它的引用计数为1(如果它的引用计数为1,则将其转换为引用)。

让我们更详细地分析这些条件。

第一个条件成立(不是函数调用)

由于额外的括号,PHP不再检测到参数是函数调用。

在解析非空函数参数列表时,PHP有三种可能性:

  • expr_without_variable
  • variable
  • &后跟一个variable,用于移除的按引用传递功能)

当只写get_array()时,PHP将其视为variable

(get_array()) 另一方面不符合 variable 的条件。它是一个 expr_without_variable

这最终影响代码编译的方式,即操作码 SEND_VAR_NO_REF 的扩展值将不再包括标志 ZEND_ARG_SEND_FUNCTION,这是在操作码实现中检测函数调用的方式。

第二点是正确的(引用计数为1)

在几个点上,Zend 引擎允许非引用的引用计数为1的情况,其中期望引用。这些细节不应该暴露给用户,但不幸的是它们在这里被曝光了。

在您的示例中,您返回一个没有从任何其他地方引用的数组。如果有,您仍将收到消息,即这第二点是不正确的。

因此,以下非常相似的示例无法工作

<?php

$a = array();
function get_array() {
   return $GLOBALS['a'];
}

return reset((get_array()));

2
太棒了。我现在意识到AST并不是特别无关紧要。谢谢 :) - Lightness Races in Orbit

1

A) 要理解这里发生的事情,就需要了解PHP对值/变量和引用的处理(PDF,1.2MB)。正如文档中所述"引用不是指针";而且你只能通过引用从函数返回变量 - 没有其他方式。

在我看来,这意味着,PHP中的任何函数都会返回一个引用。但是一些(内置的)PHP函数需要值/变量作为参数。现在,如果你嵌套函数调用,内部函数返回一个引用,而外部函数期望一个值。这导致了“著名”的E_STRICT错误"只能传递变量的引用"

$fileName = 'example.txt';
$fileExtension = array_pop(explode('.', $fileName));
// will result in Error 2048: Only variables should be passed by reference in…

B) 我在问题中链接的PHP语法描述中找到了一行。

expr_without_variable = "(" expr ")"

结合来自文档的这句话:“在PHP中,几乎所有你所编写的都是表达式。定义表达式最简单、最准确的方式是‘具有值的任何内容’。” 这使我得出结论,即使(5)在PHP中也是一个表达式,它会计算为一个整数,其值为5。
(因为$a = 5不仅仅是一个赋值操作,还是一个表达式,它的值为5。) 结论 如果你传递一个对表达式(...)的引用,这个表达式将返回一个值,然后可以将该值作为参数传递给外部函数。 如果我的想法正确,那么以下两行代码应该是等效的:
// what I've used over years: (spaces only added for readability)
$fileExtension = array_pop( ( explode('.', $fileName) ) );
// vs
$fileExtension = array_pop( $tmp = explode('.', $fileName) );

另请参阅PHP 5.0.5:致命错误:只能通过引用传递变量;13.09.2005


但从这个文档页面 http://www.php.net/manual/en/language.references.pass.php 看来,表达式不能被使用,因为结果是未定义的。我想知道整个括号技巧是否只是绕过内部检查,并且在长期内可能会成为一个未定义的应用程序结果。 - regilero
这篇文章高度推测性质。在没有文档的情况下(我已经搜索了一个多小时,知道如何使用搜索引擎),这是我能提供的最好的东西。我的想法是,共同创建一个行为文档作为SO维基条目。 - feeela
就我所知,(5) 在几乎所有类 C 语言中都是一个表达式。 - Lightness Races in Orbit
2
“在我看来,这意味着 PHP 中的任何函数都会返回一个引用。”这并不正确。该答案中有一些部分是正确的,但结论并不是从这些部分中得出的。 - Artefacto
@feeela:那不是hakre的评论吗? - Lightness Races in Orbit
显示剩余4条评论

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