让我先说一下,我知道foreach是什么、做什么以及如何使用它。这个问题涉及到它在引擎盖下的工作方式,我不希望有任何类似于“这是如何使用foreach循环数组”的答案。
在很长一段时间里,我假设foreach使用数组本身。然后,我发现了许多关于它与数组副本一起工作的事实的引用,因此我认为这就是故事的结尾。但我最近就此事进行了一次讨论,经过一点实验后发现,事实上这并非100%正确。
让我表明我的意思。对于以下测试用例,我们将使用以下阵列:
$array = array(1, 2, 3, 4, 5);
测试用例1:
foreach ($array as $item) {
echo "$item\n";
$array[] = $item;
}
print_r($array);
/* Output in loop: 1 2 3 4 5
$array after loop: 1 2 3 4 5 1 2 3 4 5 */
这清楚地表明,我们没有直接使用源数组,否则循环将永远继续,因为我们在循环期间不断将项推到数组上。但为了确保情况是这样的:
测试用例2:
foreach ($array as $key => $item) {
$array[$key + 1] = $item + 2;
echo "$item\n";
}
print_r($array);
/* Output in loop: 1 2 3 4 5
$array after loop: 1 3 4 5 6 7 */
这支持了我们最初的结论,我们在循环期间使用源数组的副本,否则我们将在循环期间看到修改的值。但是
如果我们查看手册,我们会发现以下陈述:
当foreach第一次开始执行时,内部数组指针将自动重置为数组的第一个元素。
正确的这似乎表明foreach依赖于源数组的数组指针。但我们已经证明我们没有使用源阵列,对吗?嗯,不完全是。
测试用例3:
// Move the array pointer on one to make sure it doesn't affect the loop
var_dump(each($array));
foreach ($array as $item) {
echo "$item\n";
}
var_dump(each($array));
/* Output
array(4) {
[1]=>
int(1)
["value"]=>
int(1)
[0]=>
int(0)
["key"]=>
int(0)
}
1
2
3
4
5
bool(false)
*/
因此,尽管我们不是直接使用源数组,但我们是直接使用源阵列指针-指针位于循环末尾的数组末尾这一事实表明了这一点。但这不可能是真的——如果是,那么测试用例1将永远循环。
PHP手册还指出:
由于foreach依赖于内部数组指针,因此在循环中更改它可能会导致意外行为。
好吧,让我们来看看“意外行为”是什么(从技术上讲,任何行为都是意外的,因为我不再知道会发生什么)。
测试用例4:
foreach ($array as $key => $item) {
echo "$item\n";
each($array);
}
/* Output: 1 2 3 4 5 */
测试用例5:
foreach ($array as $key => $item) {
echo "$item\n";
reset($array);
}
/* Output: 1 2 3 4 5 */
……没有什么出乎意料的,事实上,它似乎支持“复制源”理论。
问题
这是怎么回事?我的C-fu不够好,我无法通过查看PHP源代码得出正确的结论,如果有人能为我将其翻译成英语,我将不胜感激。
在我看来,foreach使用数组的副本,但在循环后将源数组的数组指针设置到数组的末尾。
这是正确的吗?如果没有,它到底在做什么?在foreach期间使用调整数组指针的函数(each()、reset()等)是否会影响循环的结果?
php7注释
更新这个答案,因为它已经获得了一些流行:这个答案从PHP7开始不再适用。正如在“向后不兼容的更改”中所解释的,在PHP7中,foreach在数组的副本上工作,因此数组本身的任何更改都不会反映在foreach循环中。更多详情请点击链接。
解释(引自php.net):
第一种形式在array_expression给定的数组上循环。在每个迭代时,当前元素的值被分配给$value内部数组指针前进一个(下一个也是如此迭代,您将看到下一个元素)。
因此,在第一个示例中,数组中只有一个元素,当指针移动时,下一个元素不存在,所以在添加新元素后,foreach结束,因为它已经“决定”将其作为最后一个元素。
在第二个示例中,从两个元素开始,foreach循环不在最后一个元素,因此它在下一次迭代时计算数组,从而意识到数组中有新元素。
我认为这是文档中解释的“每次迭代”部分的全部结果,这可能意味着foreach在调用{}中的代码之前执行所有逻辑。
测试用例
如果运行此命令:
<?
$array = Array(
'foo' => 1,
'bar' => 2
);
foreach($array as $k=>&$v) {
$array['baz']=3;
echo $v." ";
}
print_r($array);
?>
您将获得以下输出:
1 2 3 Array
(
[foo] => 1
[bar] => 2
[baz] => 3
)
这意味着它接受了修改并通过了它,因为它“及时”修改了。但如果你这样做:
<?
$array = Array(
'foo' => 1,
'bar' => 2
);
foreach($array as $k=>&$v) {
if ($k=='bar') {
$array['baz']=3;
}
echo $v." ";
}
print_r($array);
?>
您将获得:
1 2 Array
(
[foo] => 1
[bar] => 2
[baz] => 3
)
这意味着数组被修改了,但由于我们在foreach已经位于数组的最后一个元素时修改了它,所以它“决定”不再循环,即使我们添加了新元素,我们添加它“太晚了”,它也没有循环。
详细的解释可以在PHP“foreach”实际上是如何工作的?这解释了这种行为背后的内在原因。
使用foreach()时需要注意的几点:
a) foreach处理原始阵列的预测副本。这意味着foreach()将具有SHARED数据存储,除非未创建foreach Notes/User注释。
b) 是什么触发了预期拷贝?根据写时复制策略,即每当传递给foreach()的数组被更改,原始数组的克隆被创建。
c) 原始数组和foreach()迭代器将具有DISTINCT SENTINEL VARIABLES,即一个用于原始数组,另一个用于foreach;请参阅下面的测试代码。SPL、迭代器和数组迭代器。
堆栈溢出问题如何确保在PHP的“foreach”循环中重置值?解决问题的案例(3、4、5)。
以下示例显示each()和reset()不影响SENTINEL变量(例如,当前索引变量)。
$array = array(1, 2, 3, 4, 5);
list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";
foreach($array as $key => $val){
echo "foreach: $key => $val<br/>";
list($key2,$val2) = each($array);
echo "each() Original(inside): $key2 => $val2<br/>";
echo "--------Iteration--------<br/>";
if ($key == 3){
echo "Resetting original array pointer<br/>";
reset($array);
}
}
list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";
输出:
each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2
这个问题很好,因为许多开发人员,甚至是经验丰富的开发人员,都对PHP在foreach循环中处理数组的方式感到困惑。在标准foreach循环中,PHP生成循环中使用的数组的副本。循环完成后,立即丢弃副本。这在简单foreach循环的操作中是透明的。例如:
$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
echo "{$item}\n";
}
这将输出:
apple
banana
coconut
因此,创建了副本,但开发人员没有注意到,因为原始数组在循环内或循环完成后未被引用。但是,当您尝试修改循环中的项时,您会发现它们在完成以下操作时未被修改:
$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
$item = strrev ($item);
}
print_r($set);
这将输出:
Array
(
[0] => apple
[1] => banana
[2] => coconut
)
对原始文件的任何更改都不会引起注意,实际上与原始文件没有任何更改,即使您明确为$item指定了值。这是因为您正在操作$item,因为它出现在正在处理的$set副本中。您可以通过引用抓取$item来覆盖它,如下所示:
$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
$item = strrev($item);
}
print_r($set);
这将输出:
Array
(
[0] => elppa
[1] => ananab
[2] => tunococ
)
因此,当通过引用操作$item时,对$item所做的更改是对原始$set的成员所做的,这是显而易见的。通过引用使用$item也会阻止PHP创建数组副本。为了测试这一点,首先我们将显示一个演示副本的快速脚本:
$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
$set[] = ucfirst($item);
}
print_r($set);
这将输出:
Array
(
[0] => apple
[1] => banana
[2] => coconut
[3] => Apple
[4] => Banana
[5] => Coconut
)
如示例所示,PHP复制了$set并使用它进行循环,但当在循环中使用$set时,PHP将变量添加到原始数组,而不是复制的数组。基本上,PHP只使用复制的数组来执行循环和分配$item。因此,上面的循环只执行3次,每次都会在原始$set的末尾附加另一个值,使原始$set保留6个元素,但永远不会进入无限循环。
然而,如果我们像我之前提到的那样,通过引用使用$item呢?添加到上述测试的单个字符:
$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
$set[] = ucfirst($item);
}
print_r($set);
导致无限循环。请注意,这实际上是一个无限循环,您必须自己终止脚本或等待操作系统耗尽内存。我在脚本中添加了以下行,这样PHP会很快耗尽内存,如果要运行这些无限循环测试,我建议您也这样做:
ini_set("memory_limit","1M");
因此,在前面的无限循环示例中,我们看到了编写PHP以创建要循环的数组副本的原因。当创建副本并仅由循环构造本身的结构使用时,阵列在循环的整个执行过程中保持静态,因此您不会遇到问题。
php7注释
更新这个答案,因为它已经获得了一些流行:这个答案从PHP7开始不再适用。正如在“向后不兼容的更改”中所解释的,在PHP7中,foreach在数组的副本上工作,因此数组本身的任何更改都不会反映在foreach循环中。更多详情请点击链接。
解释(引自php.net):
第一种形式在array_expression给定的数组上循环。在每个迭代时,当前元素的值被分配给$value内部数组指针前进一个(下一个也是如此迭代,您将看到下一个元素)。
因此,在第一个示例中,数组中只有一个元素,当指针移动时,下一个元素不存在,所以在添加新元素后,foreach结束,因为它已经“决定”将其作为最后一个元素。
在第二个示例中,从两个元素开始,foreach循环不在最后一个元素,因此它在下一次迭代时计算数组,从而意识到数组中有新元素。
我认为这是文档中解释的“每次迭代”部分的全部结果,这可能意味着foreach在调用{}中的代码之前执行所有逻辑。
测试用例
如果运行此命令:
<?
$array = Array(
'foo' => 1,
'bar' => 2
);
foreach($array as $k=>&$v) {
$array['baz']=3;
echo $v." ";
}
print_r($array);
?>
您将获得以下输出:
1 2 3 Array
(
[foo] => 1
[bar] => 2
[baz] => 3
)
这意味着它接受了修改并通过了它,因为它“及时”修改了。但如果你这样做:
<?
$array = Array(
'foo' => 1,
'bar' => 2
);
foreach($array as $k=>&$v) {
if ($k=='bar') {
$array['baz']=3;
}
echo $v." ";
}
print_r($array);
?>
您将获得:
1 2 Array
(
[foo] => 1
[bar] => 2
[baz] => 3
)
这意味着数组被修改了,但由于我们在foreach已经位于数组的最后一个元素时修改了它,所以它“决定”不再循环,即使我们添加了新元素,我们添加它“太晚了”,它也没有循环。
详细的解释可以在PHP“foreach”实际上是如何工作的?这解释了这种行为背后的内在原因。