foreach循环和&$value的引用

27
为什么一个空的foreach循环会改变结果。
我有以下代码:
$variable = [1,2,3,4];
foreach ($variable  as $key => &$value) 
  $value ++;

var_dump($variable);

我得到的结果是:

array (size=4)
  0 => int 2
  1 => int 3
  2 => int 4
  3 => &int 5

现在,当我添加一个空的foreach循环,像这样:

$variable  = [1,2,3,4];
foreach ($variable  as $key => &$value) 
  $value ++;

foreach ($variable  as $key => $value);

var_dump($variable);

我得到了这个:

array (size=4)
  0 => int 2
  1 => int 3
  2 => int 4
  3 => &int 4

请有人解释一下,为什么在添加第二个空循环后最后一个元素没有更改,并且为什么最后一个元素前面有一个&符号?


3
+1 你遇到了一个很奇怪的问题,我也很感兴趣想要知道解释。 - Darren
正如手册中所述,在使用后应该使用unset()取消引用 ~ "警告foreach循环之后,$value的引用和最后一个数组元素仍然存在。建议通过unset()销毁它。" - Phil
1
@Phil 是的,我知道,但我只需要逻辑解释。 - Khalid
1
@Khalid 进入第二个循环时,$value 仍然是对列表中最后一个项目的引用。请考虑这一点。 - Phil
“unset()” 似乎是解决方案,但并没有解释为什么第二个(空的)foreach循环会“更改”值。它仍然被设置为引用,这是已知的。但是它是如何被更改的?这是OP的问题。 - padarom
显示剩余2条评论
6个回答

19

第一次循环结束时,$value 指向与 $variable[3] 相同的位置(它们指向内存中的同一位置):

$variable  = [1,2,3,4];
foreach ($variable  as $key => &$value) 
    $value ++;
即使这个循环已经完成,$value 仍然是一个指向与 $variable[3] 相同内存位置的引用,因此每次你将一个值存储在 $value 中,这也会覆盖为 $variable[3] 存储的值:
foreach ($variable as $key => $value);
var_dump($variable);

每次循环时,$value$variable[3]都会变成$variable中可迭代项的值。

因此,在第二个循环的第三个迭代中,$value$variable[3]通过引用变为相等的4,然后在第二个循环的第四个和最后一个迭代中,没有任何变化,因为你正在将$variable[3]的值(仍为&$value)传递给$value(仍为&$value)。

这非常令人困惑,但它甚至一点也不奇怪;这是代码的执行方式与预期完全一致。

更多信息请参阅:PHP:按引用传递


要防止这种行为,只需在每个使用后添加unset($value);语句即可。解决方法之一是将foreach循环封装在一个自调用闭包中,以强制将$value设置为局部变量,但需要的附加字符数比取消设置要多。

(function($variable){
   foreach ($variable  as $key => &$value) $value++;
})($variable);


2
这不是语言缺陷吗?$value变量应该是foreach循环的局部变量,而不应该从外部访问。 - Nuclear

13

这是一个名称冲突:在第一个循环中引入的名称 $value 存在于其后并在第二个循环中使用。因此,对它的所有赋值实际上都是对原始数组的赋值。您所做的在以下代码中更容易观察到:

  $variable = [1,2,3,4];
  foreach ($variable  as $key => &$value) 
    $value ++;
  $value = 123; // <= here you alter the array!
  var_dump($variable);

你会看到$variable[3]显示为123

避免这种情况的一种方法,正如其他人所说,是在循环后使用unset ($value),这应该是手册推荐的良好实践。另一种方法是在第二个循环中使用另一个变量:

  $variable  = [1,2,3,4];
  foreach ($variable  as $key => &$value) 
    $value ++;
  foreach ($variable  as $key => $val);
  var_dump($variable);

它不会改变您的数组。


我希望PHP和Python有一种声明新变量的方式——也就是说,只需指示我期望这个变量是新的,就像Perl(my)、Java(var)或LaTeX(\newcommand)一样。微小的额外努力,可以在调试时间和因不正确的程序而造成的损失方面获得巨大的好处。 - Alexander Gelbukh
2
@AlexanderGelbukh 我一直都这么认为!幸运的是,当 PHP 被良好编写时,数据封装和作用域可以将这些问题最小化,但不幸的是,PHP 经常被糟糕地编写... - Adelmar
1
@chjohasbrouck 部分解决方案是编写一个名为 new() 的函数,如果变量已经设置,则终止程序,并系统地使用它。这将防止在此问题中描述的情况发生。但这并不能防止拼写错误:$variable = 1; $varuable++; echo $variable;,它会毫无警告地输出 1。这样的语言创造者非常愚蠢。 - Alexander Gelbukh
@AlexanderGelbukh 这里的问题可能不仅仅是变量名称相同,还包括复制数组时。数组包含一个链接,复制数组将修改原始数组。 - Alex78191

5

在 foreach 循环后,数组的最后一个元素将保持不变。因此需要在循环外使用 unset 函数来清除它。

$variable  = [1,2,3,4];
  foreach ($variable  as $key => &$value) {
$value++;


}
  unset($value);
  var_dump($variable); 

链接手册可以在这里找到:http://php.net/manual/zh/control-structures.foreach.php

4

正如phil在评论中所说:

如手册中所述,您应该在使用后取消引用。


$variable  = [1,2,3,4];

foreach ($variable  as $key => &$value)  {
  $value ++;
}
unset($value);

foreach ($variable  as $key => $value);


print_r($variable);

将返回:

Array
(
    [0] => 2
    [1] => 3
    [2] => 4
    [3] => 5
)

例子


解释

foreach()手册中摘取的。(请看大红框

引用一个$value和最后一个数组元素的引用在foreach循环之后仍然存在。建议使用unset()将其销毁。

简单来说,这意味着:被引用的$value和数组中的最后一个元素(在本例中为4)是相同的。为了解决这个问题,在使用后必须使用unset()删除该值,否则它将保留在数组中作为其原始值(如果有意义的话)。

您也应该阅读这个链接:PHP 'foreach' 实际上是如何工作的?


最好在循环结束后将其取消设置。 - Phil
那个 unset 应该在循环外面。 - Hanky Panky
1
是的,你说得对,但你能给我解释一下为什么 foreach 循环会导致这个问题吗? - Khalid
@Phil和Hanky - 抱歉,我是从eval.in测试时拿的! - Darren
@Hanky웃Panky,我并没有看到在循环内使用它会有什么问题,除非你想进行微观优化。 - Phil
@Darren 抱歉,但我还是不明白...在第一个循环之后,$value 的值是 5(我刚测试过),第二个循环什么也没做,显然最后的值也应该是 5,为什么会是 4 - Khalid

4
循环结束后,您应该使用以下代码取消此引用:

unset

unset($value);

所以您的整个代码应该像这样工作:
  $variable  = [1,2,3,4];
  foreach ($variable  as $key => &$value) {
    $value++;
  }
  unset($value);
  var_dump($variable); 

在循环内部放置"unset($value);"是没有意义的。
解释:循环结束后,$value仍然设置为数组的最后一个元素,因此您可以在循环后使用"$value = 10;"(在unset之前),您会看到数组的最后一个元素已更改为“10”。在这种情况下,似乎"var_dump"希望为我们提供一些帮助,并显示最后一个元素的引用,当然,当我们使用"unset"时,就会得到所需的"var_dump"输出。
您也可以查看以下脚本:
<?php
  $array = [1, 2, 3, 4];

  var_dump($array);

  $x = &$array[2];

  var_dump($array);

  $x += 20;

  unset($x);

  var_dump($array);

?>

在这里我们不使用循环,如果引用被设置为数组元素,则var_dump会在该元素类型之前放置&以显示该内容。

然而,如果我们改变引用并像这样设置$x = &$array;,var_dump就不会显示任何引用。

还有下面的代码:

<?php
$x = 23;
$ref = &$x;

var_dump($x);

?>

var_dump()不会给我们任何提示。


2
@Hanky웃Panky,这是因为我们不是在寻找解决方案,而是在寻找解释。 - Khalid
@Khalid 我已添加了解释 - Marcin Nabiałek
@MarcinNabiałek,你说得对,在第一个循环之后分配一个值,最后一个元素会改变。我理解为什么数组的最后一个元素是一个引用...但是第二个循环怎么会使最后一个元素像前面的那个? - Khalid
@Khalid,我认为没有必要考虑这个问题。只是因为你在第一个循环后没有取消引用,所以代码是错误的。对于我来说,想知道它为什么会这样工作是没有意义的。 - Marcin Nabiałek
@MarcinNabiałek 了解为什么应该遵循特定的建议对某些人有所帮助 :) - Ja͢ck

3

必须声明:引用是邪恶的!

逐步解析您的代码:

$variable  = [1,2,3,4];
foreach ($variable  as $key => &$value) 
    $value++;

循环完成后,$value 是指向 $variable[3] 的引用,因此具有值 int(4)
foreach ($variable as $key => $value);

每次迭代,$variable[3]被赋值为$variable[<k>]元素之一,其中 0 <= k < 3。在最后一次迭代中,它被赋值为上一次迭代的值,即int(4)

在两个循环之间取消设置 $value 可以解决这个问题。另请参见我的 早期答案


@Khalid,这是一个非常不幸的引用设计...顺便说一下,请停止移动已接受的答案,只能有一个被接受 :) - Ja͢ck
抱歉,我只是把它返回给第一个回答正确的人 :)!再次对你的帮助表示抱歉和感谢 :) - Khalid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接