【问题标题】:Parentheses altering semantics of function call result括号改变函数调用结果的语义
【发布时间】:2011-10-07 06:46:52
【问题描述】:

another question 中指出,将 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 语法及其对语句/表达式的处理知之甚少,无法自己派生它。

文档中是否隐藏了有关此行为的任何内容?如果不是,其他人可以不依靠假设来解释吗?


更新

我首先发现this EBNF声称代表PHP语法,并尝试自己解码我的脚本,但最终放弃了。

然后,using phc 生成两个foo() 变体的.dot 文件,我使用以下命令为两个脚本的produced 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

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

【问题讨论】:

  • 看起来这是专门针对单个函数调用形式的表达式。
  • Array() 带有大写 A? afaik,语言结构写成array()
  • PHP,因此不区分大小写。
  • 只有一个函数调用可以有这个的原因是只有一个变量或一个通过引用返回的函数可以reset的正确输入。一个变量显然总是通过引用工作,这给我们留下了只在执行时检查的函数调用,因为可能有类似$variablewithafunctionname()的东西。为什么() 会让reset 不抱怨...这意味着在reset 获得其输入时它 引用(refcount > 1),这意味着表达式@ 987654339@ 在内存中留下一些 zval...
  • 进一步挖掘,严格的警告来自 VM 部分/运行时。致命错误(不在 Q 的示例中,一个是:return reset((get_array()?:0));)已经在编译时,措辞更加苛刻:“致命错误:只能通过引用传递变量” (而且错了,如果一个函数返回一个引用,那一切都很好)。在发出严格通知之前检查了许多标志,我在其中的某个地方闻到了它,但我对 PHP 内部了解不多:php-trunk/Zend/zend_vm_execute.h 第 10853 行~

标签: php php-internals


【解决方案1】:

这种行为可以归类为bug,所以你绝对不应该依赖它。

在函数调用中抛出not消息的(简化)条件如下(参见definition of the opcode ZEND_SEND_VAR_NO_REF):

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

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

第一点为真(不是函数调用)

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

在解析non empty function argument list 时,PHP 有三种可能性:

  • expr_without_variable
  • 一个variable
  • (一个&amp;,后跟一个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()));

【讨论】:

  • 太棒了。我现在意识到 AST 并不是特别无关紧要。谢谢你:)
【解决方案2】:

A) 要了解这里发生的事情,需要understand PHP's handling of values/variables and references(PDF,1.2MB)。如stated throughout the documentation"references are not pointers";和you can only return variables by reference from a function - 没有别的。

在我看来,这意味着 PHP 中的任何函数都会返回一个引用。但有些函数(内置于 PHP)需要 values/variables 作为参数。现在,如果您正在嵌套函数调用,则内部函数调用返回一个引用,而外部函数调用需要一个值。这会导致“著名的”E_STRICT-error "Only variables should be passed by reference"

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

B) 我在PHP-syntax description linked in the question 中找到了一行。

expr_without_variable = "(" expr ")"

结合documentation 中的这句话:“在 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: Fatal error: Only variables can be passed by reference; 13.09.2005

【讨论】:

  • 但是从这个文档页面:php.net/manual/en/language.references.pass.php 似乎不能使用表达式“因为结果是未定义的”。我想知道整个括号技巧是否不仅仅是绕过内部检查,并且从长远来看可能会成为未定义的应用程序结果。
  • 这篇文章是高度投机的。在没有文档的情况下(我已经搜索了一个多小时,知道如何使用搜索引擎),这是我能提供的最好的。我的想法是,通常为该行为创建一个文档作为 SO-wiki 条目。
  • FWIW, (5) 几乎是所有类 C 语言中的表达式。
  • -1 "IMO,也就是说,PHP 中的任何函数都会返回一个引用。"不是真的。答案有一些段落是正确的,但从这些段落中永远不会得出结论。
  • 我认为“Bug”根本不是一个分类。这是一个不精确的术语,通常与“特征”一致。您会将所描述的行为归类为 PHP 编程语言中的 "Fault" 吗?
猜你喜欢
  • 1970-01-01
  • 2010-10-10
  • 1970-01-01
  • 2020-09-18
  • 2011-09-28
  • 1970-01-01
  • 2012-04-27
  • 2016-06-27
相关资源
最近更新 更多