PHP Foreach通过参考传递:最后一个元素重复吗?(臭虫?)


159

我刚编写的简单php脚本有一些非常奇怪的行为。我将其减少到重新创建该错误所需的最低限度:

<?php

$arr = array("foo",
             "bar",
             "baz");

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

?>

输出:

Array
(
    [0] => foo
    [1] => bar
    [2] => baz
)
Array
(
    [0] => foo
    [1] => bar
    [2] => bar
)

这是应该发生的错误还是某些真正奇怪的行为?


再次按值执行,看看是否第三次更改...?
Shackrock

1
@Shackrock,按值重复循环似乎不再改变。
regality 2011年

1
有趣的是,如果将第二个循环更改为使用$ item以外的其他东西,则它将按预期工作。
史蒂夫·克拉里奇

9
总是在循环主体的末尾取消设置该项目:foreach($x AS &$y){ ... unset($y); }-实际上在php.net上(不知道在哪里),因为这是一个很容易犯的错误。
Rudie 2011年

Answers:


170

在第一个foreach循环之后,$item仍然是对的引用,该值也被所使用$arr[2]。因此,第二个循环中的每个foreach调用(不是通过引用调用)都会替换该值,并因此替换$arr[2]为新值。

因此,循环1的值$arr[2]变成了$arr[0]'foo'。
循环2,值和$arr[2]变为$arr[1],即“ bar”。
循环3,值and $arr[2]变成$arr[2]'bar'(由于循环2)。

实际上,在第二个foreach循环的第一次调用中,值“ baz”实际上丢失了。

调试输出

对于循环的每次迭代,我们将回显的值,$item并递归地打印array $arr

运行第一个循环时,我们看到以下输出:

foo
Array ( [0] => foo [1] => bar [2] => baz )

bar
Array ( [0] => foo [1] => bar [2] => baz )

baz
Array ( [0] => foo [1] => bar [2] => baz )

在循环结束时,$item仍指向与相同的位置$arr[2]

当第二个循环运行时,我们看到以下输出:

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

您会注意到每个时间数组如何将一个新值放入中$item,它也会$arr[3]使用相同的值进行更新,因为它们都仍指向相同的位置。当循环到达数组的第三个值时,它将包含该值,bar因为它只是由该循环的上一次迭代设置的。

是虫子吗?

否。这是引用项目的行为,而不是错误。这将类似于运行以下内容:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

foreach循环本质上并不特殊,它可以忽略引用的项目。就像在循环外一样,每次都只是将变量设置为新值。


4
我有一个轻微的脚矫正。$item不是对的引用$arr[2],所包含的值$arr[2]是对所引用的值的引用$item。为了说明不同之处,您还可以设置unset $arr[2],并且$item不受影响,并且写入$item不会影响它。
Paul Biggar

2
这种行为很难理解,并可能导致问题。我将其作为我的最爱之一,向我的学生们展示为什么他们应该(只要有可能)避免“参考”。
奥利维尔·庞斯

1
为什么$item退出foreach循环时不超出范围?这似乎是关闭问题?
jocull

6
@jocull:在PHP,foreach,for,while等中,不会创建自己的作用域。
animuson

1
@ jocull,PHP没有(阻止)局部变量。它惹恼我的原因之一。
Qtax

29

$item$arr[2]正如animuson所指出的,它是对第二个foreach循环的引用,并被其覆盖。

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

unset($item); // This will fix the issue.

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

3

虽然这可能并不是正式的错误,但我认为是。我认为这里的问题是我们期望$item退出循环时会超出范围,就像许多其他编程语言一样。但是,事实似乎并非如此……

此代码...

$arr = array('one', 'two', 'three');
foreach($arr as $item){
    echo "$item\n";
}    
echo $item;

给出输出...

one
two
three
three

正如其他人已经说过的那样,您将在$arr[2]第二个循环中覆盖引用的变量,但这之所以会发生是因为$item从未超出范围。你们怎么看……臭虫?


4
1)不是错误。手册中已经对此进行了标注,并按预期在许多错误报告中将其删除。2)并没有真正回答这个问题……
BoltClock

它不是因为范围问题而引起我的注意,我希望$ item在最初的foreach之后仍然存在,但是我没有意识到foreach会更新变量而不是替换它。例如,与在第二个循环之前运行unset($ item)相同。请注意,未设置不会清除值(因此也不会清除数组中的最后一个元素),而是仅删除变量。
Programster

不幸的是,PHP通常不会为循环或{}块创建新的作用域。这就是语言的工作方式
Fabian Schmengler


0

在我看来,PHP的正确行为应该是一个NOTICE错误。如果在循环外部使用在foreach循环中创建的引用变量,则应引起注意。这种行为很容易掉下去,发生时很难发现。而且没有开发人员会阅读foreach文档页面,这没有帮助。

您应该unset()在循环后引用以避免这种问题。引用上的unset()只会删除引用,而不会损害原始数据。


0

那是因为您使用了ref指令(&)。最后一个值将被第二个循环替换,它将破坏您的数组。最简单的解决方案是对第二个循环使用不同的名称:

foreach ($arr as &$item) { ... }

foreach ($arr as $anotherItem) { ... }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.