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

Posted

技术标签:

【中文标题】这种 PHP 按值调用的行为有合理的解释吗?还是 PHP 错误?【英文标题】:Is there a rational explanation for this PHP call-by-value behavior? Or PHP bug? 【发布时间】:2014-09-06 12:28:14 【问题描述】:

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
)

对此有合理的解释吗?

【问题讨论】:

在我看来 $a is 在示例 #2 中通过引用传递。这确实与 PHP 5.4 ~ eval.in/172839 中发生的情况不同。可能与 5.5 中引入的数组取消引用更改有关 您的最后一个示例无效。 $b = z($a); 立即应用于整个数组。 来自these results 很明显,您实际上不应该依赖这种行为;事实上,这可能是一个错误。 再给你一个古怪的例子~eval.in/172857 @Nairebis 刚刚与另一位 php-src 开发人员快速聊天,他同意这是一个错误。 【参考方案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”,表明它们不是副本或克隆。

【讨论】:

但是在 PHP 中,对象是通过引用隐式传递的,而数组不是(意味着)所以你的例子不准确。不过,您可能会对 “函数 z 什么都不做” 有所了解。可能只是工作中的编译器优化 @Phil 我同意 Phil 的观点,即观察到的行为可能是优化的结果,恕我直言,不应该以这种方式工作。 您正在传递和返回对对象的引用,而不是对象本身。引用确实被复制了。【参考方案2】:

更新

已打开错误67633 以解决此问题。 this commit 已更改此行为,以消除 foreach 的引用限制。


从this 3v4l output 你可以清楚地看到这种行为随着时间的推移发生了变化:

更新 2

固定为this commit;这将在 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 块内使用时,这些版本不执行写时复制。因此,现在使用原始数组,并且对元素的更改是永久性的。

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

PHP > 5.6

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

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

Proof

破解 (HHVM)

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

正确的方法

documented通过引用使用函数结果的方式是这样的:

$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;

Demo

其他修复

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

    将函数结果赋值给一个临时变量之前foreach; 不要使用引用; 切换到 Hack。

【讨论】:

确实,根据手册,这也不应该工作~eval.in/172864 我已经确认这已在 5.5.19 中修复(ubuntu 14.04 64bit via ppa:ondrej/php5)

以上是关于这种 PHP 按值调用的行为有合理的解释吗?还是 PHP 错误?的主要内容,如果未能解决你的问题,请参考以下文章

PHP 变量是按值传递还是按引用传递?

Python 元类行为(不调用 __new__),有解释吗?

PHP - file_exists 还是数组缓存?

Python 的布尔值是按值传递的吗?

C ++接口类继承自具体类是否合理?

JavaScript 是按引用传递还是按值传递? [复制]