对于这种PHP按值调用的行为,是否有合理的解释?还是PHP错误?

Is there a rational explanation for this PHP call-by-value behavior? Or PHP bug?

提问人:Nairebis 提问时间:7/16/2014 最后编辑:Nairebis 更新时间:9/22/2014 访问量:1869

问:

PHP 5.5.12.考虑一下:

<?php
$a = [ 'a', 'b', 'c' ];
foreach($a as &$x) {
    $x .= 'q';
}
print_r($a);

正如预期的那样,这将输出:

Array
(
    [0] => aq
    [1] => bq
    [2] => cq
)

现在考虑:

<?php
$a = [ 'a', 'b', 'c' ];
foreach(z($a) as &$x) {
    $x .= 'q';
}
print_r($a);

function z($a)
{
    return $a;
}

这将输出:

Array
(
    [0] => aq
    [1] => bq
    [2] => cq
)

(!)但是等一下。$a不是通过引用传递的。这意味着我应该从 z() 取回一个副本,该副本将被修改,$a应该单独保留。

但是,当我们强迫PHP施展其复制写入的魔力时会发生什么:

$a = [ 'a', 'b', 'c' ];
foreach(z($a) as &$x) {
    $x .= 'q';
}
print_r($a);

function z($a)
{
    $a[0] .= 'x';
    return $a;
}

为此,我们得到了我所期望的:

Array
(
    [0] => a
    [1] => b
    [2] => c
)

编辑:再举一个例子......

$a = [ 'a', 'b', 'c' ];
$b = z($a);
foreach($b as &$x) {
    $x .= 'q';
}
print_r($a);

function z($a)
{
    return $a;
}

这按预期工作:

Array
(
    [0] => a
    [1] => b
    [2] => c
)

对此有合理的解释吗?

PHP 数组 引用 传递 引用

评论

1赞 Phil 7/16/2014
在我看来,就像在示例 #2 中通过引用传递一样。这确实与 PHP 5.4 ~ eval.in/172839 中发生的情况不同。可能与 5.5 中引入的数组取消引用更改有关$a
0赞 Hanky Panky 7/16/2014
最后一个示例无效。 正在同时应用于整个数组。$b = z($a);
1赞 Ja͢ck 7/16/2014
这些结果中可以明显看出,您实际上不应该依赖这种行为;事实上,这可能是一个错误。
0赞 Phil 7/16/2014
为你准备了另一个古怪的例子~eval.in/172857
2赞 Ja͢ck 7/16/2014
@Nairebis 刚刚与另一位 php-src 开发人员进行了简短的聊天,他同意这是一个错误。

答:

-1赞 t j 7/16/2014 #1

在此示例中,函数 z 不执行任何操作。它不会复制或克隆任何内容,因此来自 z() 的响应将与根本不调用相同。您只是返回传入的对象,因此响应符合预期。

<?php
$a = [ 'a', 'b', 'c' ];
foreach(z($a) as &$x) {
    $x .= 'q';
}
print_r($a);

function z($a)
{
    return $a;
}

Thiis 更容易使用对象进行演示,因为它们被赋予了系统 ID:

<?php
$obj = new stdClass();
$obj->name = 'foo';

function z($a)
{
    $a->name = 'bar';
    return $a;
}

var_dump($obj);
var_dump(z($obj));

其输出为:

object(stdClass)#1 (1) {
  ["name"]=>
  string(3) "foo"
}
object(stdClass)#1 (1) {
  ["name"]=>
  string(3) "bar"
}

这两个对象的 ID 都为“1”,表明它们不是副本或克隆。

评论

1赞 Phil 7/16/2014
但是在 PHP 中,对象是通过引用隐式传递的,而数组不是(本来是),所以你的例子是不准确的。不过,您可能会遇到“函数 z 什么都不做”的事情。可能只是编译器优化在起作用
0赞 Ja͢ck 7/16/2014
@Phil 我同意菲尔的观点,即观察到的行为可能是优化的结果,恕我直言,不应该以这种方式工作。
0赞 newacct 7/17/2014
您传递和返回的是对象的引用,而不是对象本身。引用确实是复制的。
9赞 Ja͢ck 7/16/2014 #2

更新

已打开 Bug 67633 以解决此问题。此提交已更改该行为,以消除 foreach 的引用限制。


这个 3v4l 输出中,您可以清楚地看到此行为已随时间而改变:

更新 2

通过此提交修复;这将在 5.5.18 和 5.6.2 中提供。

PHP 5.4

在 PHP 5.5 之前,您的代码实际上会引发一个致命错误:

Fatal error: Cannot create references to elements of a temporary array expression

PHP 5.5 - 5.6

当函数结果直接在块内使用时,这些版本不执行写入时复制。因此,现在使用原始数组,并且对元素的更改是永久性的。foreach

我个人觉得这是一个错误;应该已经发生了写时复制。

PHP> 5.6

phpng 分支中,它可能会成为下一个主要版本的基础,常量数组是不可变的,因此只有在这种情况下才能正确执行写入时复制。像下面这样声明数组将出现与 phpng 相同的问题:

$foo = 'b';
$a = ['a', $foo, 'b'];

证明

黑客 (HHVM)

只有 Hack 才能正确处理当前的情况。

正确的方式

通过引用使用函数结果的记录方式如下:

$a = [ 'a', 'b', 'c' ];
foreach(z($a) as &$x) {
    $x .= 'q';
}
print_r($a);

// indicate that this function returns by reference 
// and its argument must be a reference too
function &z(&$a)
{
    return $a;
}

演示

其他修复

为了避免更改原始数组,目前,您有以下选项:

  1. 将函数结果赋值到一个临时变量中,然后foreach;
  2. 不要使用引用;
  3. 切换到 Hack。

评论

0赞 Phil 7/16/2014
事实上,根据手册,这也应该行不通~eval.in/172864
0赞 rynop 12/11/2014
我已经确认这在 5.5.19(ubuntu 14.04 64 位通过 ppa:ondrej/php5)中已修复