带有引用参数的 foreach 何时危险?

Posted

技术标签:

【中文标题】带有引用参数的 foreach 何时危险?【英文标题】:When is foreach with a parameter by reference dangerous? 【发布时间】:2018-06-18 19:18:18 【问题描述】:

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

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

$array = ['test'];
foreach ($array as &$item)
    $item = $item;

$item = 'modified';
var_dump($array);

数组(1) [0]=> &string(8) "修改"

现在这让我很生气:数组的内容在函数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);

数组(1) [0]=> &string(8) "修改"

我很想检查我的整个代码库并在每个 foreach($array => &$item) 之后插入 unset($item);

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

编辑澄清

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

foreach($array as &$item)之后我知道这很危险:

重用变量$item 将数组传递给函数

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

【问题讨论】:

“危险”是主观的。有时它是有用的或必要的。 @tadman 是的,就像一把刀:它通常有用且必要,但也很危险。 @poke 你错了,***.com/questions/2030906/…,数组是按值传递的。在我的例子中,数组是按值传递的,但里面的$itemforeach 之后仍然是一个引用。 按值传递的数组。看那里 - eval.in/932720 你的问题是分配 $item - 当 $item 是参考时,你保存的不是价值,而是参考。注意var_dump中的& 好吧,现在我真的很困惑。这根本不是我认为 php 工作的方式,而且它实际上与引用循环无关。 any 引用存在于函数外部的数组是一个问题:请参阅eval.in/932728 【参考方案1】:

关于foreach

首先,关于 PHP 的两种行为的一些(可能是显而易见的)澄清:

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

    当变量是引用时,则赋值,例如$item = 'foo'; 将更改引用指向的任何内容,而不是变量 ($item) 本身。 对于后续的 foreach($array2 as $item) 也是如此,如果 $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"

但是请注意,数组内的引用具有潜在的危险。使用右侧的引用进行普通(非引用)赋值不会将左侧变为引用,但数组内的引用会保留在这些普通赋值中。 这也适用于数组按值传递的函数调用。 [...] 换句话说,数组的引用行为是在逐个元素的基础上定义的;单个元素的引用行为与数组容器的引用状态是分离的。

使用以下示例(复制/粘贴):

/* 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] 都会全局修改它。

结论

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

我将再次重复 PHP 文档中的引用:“数组内的引用具有潜在危险”。

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

【讨论】:

【参考方案2】:

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

$item: 以后再不重复使用也无妨。

$array:最后一个元素是引用。这总是很危险的,原因已经说明了。

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

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

当数组从函数返回时$item超出范围,则数组从函数返回后变为“正常”。

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

$a = test();
var_dump($a);

数组(1) [0]=> &int(1) 数组(1) [0]=> 整数(1)

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

【讨论】:

【参考方案3】:

您可以通过“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);

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

当然它会输出:

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

另一方面,同样的事情也适用于 javascript,它也可以让你从引用中获得一些奇怪的东西。

我希望我有一个很好的例子,因为我发生了一些“奇怪”的事情,我的意思是一些量子纠缠的事情。这一次在 PHP 训练营中,我有一个递归函数(通过引用传递)和一个 foreach(通过引用传递),它有点像在时空连续体中撕裂了一个洞。

【讨论】:

在 foreach 循环之后使用 unset($item);json_decode(json_encode()) 好得多,无论是从性能角度还是代码可读性。另外,修改函数是错误的,因为函数应该保持通用。 问题是为什么函数中有一个引用,而不是如何“打破”它。 @iainn - 我完全理解这一点。但是谢谢你提到它。

以上是关于带有引用参数的 foreach 何时危险?的主要内容,如果未能解决你的问题,请参考以下文章

python如何决定何时按值传递参数以及何时按引用传递参数? [复制]

从 ForEach 中引用变量时,SwiftUI“无法推断通用参数'数据'”

何时在启用可空引用类型的情况下对参数进行空检查

C#值参数和引用参数,方法的重载,foreach,数组,以及ref和out的用法

指针能作为引用参数吗?

何时使用SESSION以及何时在Web应用程序中使用GET进行参数传递?