对于这种PHP按值调用的行为是否有合理的解释?或者PHP bug


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

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
)

对此有合理的解释吗?

更新

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

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

我个人觉得这是一个bug;Copy-on-write应该已经发生了。

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. 切换到Hack

在这个例子中,函数z什么都不做。它不会复制或克隆任何东西,因此来自z()的响应将与根本不调用相同。您只是返回传入的对象,因此响应是预期的。

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

使用对象更容易演示,因为它们被赋予了一个系统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",这表明它们不是副本或克隆。