什么时候带有引用参数的 foreach 是危险的?

When is foreach with a parameter by reference dangerous?

我知道,在 foreach 中通过引用传递项目可能很危险。

特别是,不能重用通过引用传递的变量,因为它会影响 $array,如本例所示:

$array = ['test'];
foreach ($array as &$item){
    $item = $item;
}
$item = 'modified';
var_dump($array);

array(1) { [0]=> &string(8) "modified" }

现在这让我很不爽:数组的内容在函数 should_not_modify 内部被修改,即使我没有按值传递 $array

function should_not_modify($array){
    foreach($array as &$item){
        $item = 'modified';
    }
}
$array = ['test'];
foreach ($array as &$item){
    $item = (string)$item;
}
should_not_modify($array);
var_dump($array);

array(1) { [0]=> &string(8) "modified" }

我很想遍历我的整个代码库并在每个 foreach($array => &$item).

之后插入 unset($item);

但是,由于这是一项艰巨的任务,并且引入了一条可能无用的行,我想知道是否有一个简单的规则可以知道 foreach($array => &$item) 何时安全且后面没有 unset($item); , 而当不是时。

编辑澄清

我想我明白会发生什么以及为什么。我也知道什么是最好的对抗:foreach($array as &$item){...};unset($item);

我知道在 foreach($array as &$item):

之后这很危险

我的问题是:是否还有其他危险情况,我们能否建立一份详尽的危险清单。或者反过来:是否可以描述什么时候不危险。

您可以通过 "json decode/encode"

打破引用
function should_not_modify($array){
    $array = json_decode(json_encode($array),false);
    foreach($array as &$item){
        $item = 'modified';
    }
}
$array = ['test'];
foreach ($array as &$item){
    $item = (string)$item;
}
should_not_modify($array);
var_dump($array);

这个问题纯粹是学术性的,这有点hack。但是,它以一种愚蠢的编程方式很有趣。

当然它输出:

array(1) {
  [0]=>string(4) "test"
}

另一方面,同样的事情在 JavaScript 中起作用,这也可以让您从参考文献中得到一些靠不住的东西。

我希望我有一个很好的例子,因为我遇到了一些 "weird" 事情,我的意思是像一些量子纠缠。这一次在 PHP 营地,我有一个递归函数(通过引用传递)和一个 foreach(通过引用传递)并且它有点在 space 时间连续体中撕开一个洞。

关于foreach

首先,对 PHP 的两个行为进行一些(可能是显而易见的)澄清:

  1. foreach($array as $item) 将在循环后保持变量 $item 不变。如果变量是引用,如 foreach($array as &$item),即使在循环之后,它也会 "point" 到数组的最后一个元素。

  2. 当一个变量是一个引用然后赋值,例如$item = 'foo'; 将更改引用指向的任何内容, 而不是 变量 ($item) 本身。 对于后续的 foreach($array2 as $item) 也是如此,它将 $item 视为参考,如果它是这样创建的,因此将修改参考指向的任何内容(在本例中,前面foreach中使用的数组的最后一个元素)。

显然这很容易出错,这就是为什么您应该始终 unsetforeach 中使用的引用以确保后续写入不会修改最后一个元素(如 example #10 类型数组的文档)。

关于修改数组的函数

值得注意的是 - 正如@iainn 在评论中指出的那样 - 您示例中的行为与 foreach 无关。仅存在对数组元素的引用就可以修改该元素。示例:

function should_not_modify($array){
    $array[0] = 'modified';
    $array[1] = 'modified2';
}
$array = ['test', 'test2'];
$item = & $array[0];

should_not_modify($array);
var_dump($array);

将输出:

array(2) {
  [0] =>
  string(8) "modified"
  [1] =>
  string(5) "test2"
}

诚然,这非常令人惊讶,但 explained in the PHP documentation "What References Do"

Note, however, that references inside arrays are potentially dangerous. Doing a normal (not by reference) assignment with a reference on the right side does not turn the left side into a reference, but references inside arrays are preserved in these normal assignments. This also applies to function calls where the array is passed by value. [...] In other words, the reference behavior of arrays is defined in an element-by-element basis; the reference behavior of individual elements is dissociated from the reference status of the array container.

以下面的例子(copy/pasted):

/* Assignment of array variables */
$arr = array(1);
$a =& $arr[0]; //$a and $arr[0] are in the same reference set
$arr2 = $arr; //not an assignment-by-reference!
$arr2[0]++;
/* $a == 2, $arr == array(2) */
/* The contents of $arr are changed even though it's not a reference! */

重要的是要了解在创建引用时,例如 $a = &$b 那么 $a$b 是相等的。 $a 没有指向 $b ,反之亦然。 $a$b 指向同一个地方。

因此,当您执行 $item = & $array[0]; 时,您实际上使 $array[0] 指向与 $item 相同的位置。由于 $item 是一个全局变量,并且保留了数组内部的引用,因此从任何地方(甚至从函数内部)修改 $array[0] 都会在全局范围内修改它。

结论

Are there other cases that are dangerous, and can we build an exhaustive list of what is dangerous. Or the other way round: is it possible to describe when it is not dangerous.

我要再次引用 PHP 文档中的话:"references inside arrays are potentially dangerous".

所以不,不可能描述什么时候不危险,因为它永远危险。很容易忘记 $item 已创建为引用(或已创建但未销毁的全局引用),并在代码的其他地方重用它并破坏数组。这一直是争论的话题(在 this bug for example 中),人们称它为错误或功能...

接受的答案是最好的,但我想补充一下:foreach($array as &$item) 之后什么时候不需要 unset($item);

  • $item: 以后不重复使用,就不会造成伤害。

  • $array:最后一个元素是引用。出于上述所有原因,这总是很危险。

那么是什么改变了元素形式成为对值的引用?

  • 被引用次数最多:unlink($item);

  • 当数组从函数 return 编辑时 $item 超出范围,则数组在 return 之后变为 'normal'来自函数。

    function test(){
        $array = [1];
        foreach($array as &$item){
            $item = $item;
        }
        var_dump($array);
        return $array;
    }
    $a = test();
    var_dump($a);
    

    array(1) { [0]=> &int(1) }
    array(1) { [0]=> int(1) }

    但要注意: 如果你在 return 之前做任何其他事情,它可能会咬人!