PHP的foreach实际上是如何工作的?

让我先说这个,我知道什么是foreach ,是否和如何使用它。 这个问题涉及到它是如何在引擎盖下工作的,我不希望有任何答案,“这是如何循环使用foreach的数组”。


很长一段时间,我认为foreach与数组本身一起工作。 然后,我发现它提供了许多与该数组副本一起工作的事实,而我从此认为这是故事的结尾。 但是最近我就这个问题进行了讨论,经过一番小小的实验,发现事实上并不是100%的事实。

让我表明我的意思。 对于以下testing用例,我们将使用以下数组:

 $array = array(1, 2, 3, 4, 5); 

testing案例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 */ 

这清楚地表明我们不直接使用源数组 – 否则循环会一直持续下去,因为我们在循环过程中不断地将项目推到数组上。 但是可以肯定的是,

testing案例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依赖于源数组的数组指针。 但是,我们刚刚certificate,我们没有使用源数组 ,对吗? 那么,不完全。

testing案例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) */ 

所以,尽pipe我们不直接使用源数组,但是我们直接使用源数组指针 – 指针位于循环结尾的数组末尾的事实显示了这一点。 除了这不可能是真的 – 如果是的话,那么testing用例1将永远循环。

PHP手册还指出:

由于foreach依靠内部数组指针在循环内改变它可能会导致意外的行为。

那么,让我们来看看那个“意外的行为”是什么(从技术上讲,任何行为都是意外的,因为我不知道该期待什么)。

testing案例4 :

 foreach ($array as $key => $item) { echo "$item\n"; each($array); } /* Output: 1 2 3 4 5 */ 

testing案例5 :

 foreach ($array as $key => $item) { echo "$item\n"; reset($array); } /* Output: 1 2 3 4 5 */ 

……没有什么意外的,实际上它似乎支持“抄袭”的理论。


问题

这里发生了什么? 我的C-fu对我来说只是通过查看PHP源代码来获得正确的结论还不够好,如果有人能把它翻译成英文,我将不胜感激。

在我看来, foreach与数组的副本一起工作,但在循环之后将源数组的数组指针设置为数组的末尾。

  • 这是正确的,整个故事?
  • 如果不是,那真的是在干什么?
  • foreach中使用调整数组指针( each()reset()each()函数是否会影响循环的结果?

foreach支持三种不同types的值的迭代:

  • 数组
  • 正常的对象
  • Traversable对象

下面我将尝试解释在不同的情况下迭代是如何工作的。 到目前为止,最简单的情况是Traversable对象,因为对于这些foreach本质上只是代码的语法糖:

 foreach ($it as $k => $v) { /* ... */ } /* translates to: */ if ($it instanceof IteratorAggregate) { $it = $it->getIterator(); } for ($it->rewind(); $it->valid(); $it->next()) { $v = $it->current(); $k = $it->key(); /* ... */ } 

对于内部类来说,实际的方法调用是通过使用一个内部的API来避免的,这个API本质上只是镜像C级的Iterator接口。

数组和平面对象的迭代显着更复杂。 首先,应该注意的是,在PHP中,“数组”实际上是有序的字典,它们将按照这个顺序遍历(只要你不使用类似的东西,就与插入顺序相匹配)。 这与按键的自然顺序(其他语言中的列表经常工作)或者根本没有定义的顺序(其他语言的字典经常工作)是相反的。

同样也适用于对象,因为对象属性可以看作是将属性名称映射到其值的另一(sorting)字典,以及一些可见性处理。 在大多数情况下,对象属性实际上并不是以这种效率低下的方式存储的。 但是,如果您开始迭代对象,则通常使用的打包表示将转换为实际字典。 在这一点上,简单对象的迭代变得非常类似于数组的迭代(这就是为什么我不在这里讨论简单对象迭代的原因)。

到现在为止还挺好。 迭代字典不能太难,对吧? 当你意识到一个数组/对象可以在迭代过程中改变时,问题就开始了。 有多种方式可以发生:

  • 如果使用foreach ($arr as &$v)通过引用进行迭代,则$arr将变为引用,并且可以在迭代过程中对其进行更改。
  • 在PHP 5中,即使按值迭代也是如此,但数组事先是一个引用: $ref =& $arr; foreach ($ref as $v) $ref =& $arr; foreach ($ref as $v)
  • 对象具有by-handle传递语义,这对于实际的目的来说意味着它们像引用一样。 所以在迭代期间总是可以改变对象。

在迭代过程中允许修改的问题是当前所在元素被删除的情况。 假设你使用一个指针来跟踪你当前在哪个数组元素。 如果这个元素现在被释放,那么你只剩下一个悬挂指针(通常导致段错误)。

解决这个问题有不同的方法。 PHP 5和PHP 7在这方面差异很大,我将在下面描述这两种行为。 总结一下,PHP 5的方法相当愚蠢,会导致各种奇怪的边缘案例问题,而PHP 7的更多涉及的方法会产生更可预测和一致的行为。

作为最后的初步,应该注意的是,PHP使用引用计数和写时复制来pipe理内存。 这意味着如果你“复制”一个值,你实际上只是重用旧的值,并增加其引用计数(refcount)。 只有当你执行某种修改时,才会完成一个真正的副本(称为“重复”)。 请参阅您对此主题进行更广泛的介绍。

PHP 5

内部数组指针和HashPointer

PHP 5中的数组有一个专用的“内部数组指针”(IAP),它可以很好地支持修改:每当一个元素被移除时,将会检查IAP是否指向这个元素。 如果是这样的话,它会被推进到下一个元素。

虽然foreach确实使用了IAP,但还有一个额外的复杂性:只有一个IAP,但是一个数组可以是多个foreach循环的一部分:

 // Using by-ref iteration here to make sure that it's really // the same array in both loops and not a copy foreach ($arr as &$v1) { foreach ($arr as &$v) { // ... } } 

为了支持只有一个内部数组指针的两个同时循环,foreach执行以下schenanigans:在执行循环体之前,foreach将备份一个指向当前元素的指针,并将其散列到每个foreach HashPointer 。 在循环体运行之后,如果IAP仍然存在,它将被设置回这个元素。 如果元素已经被删除,我们将使用IAP当前所处的位置。 这个scheme大多是有点类似的,但是你可以从中得到很多奇怪的行为,其中一些我将在下面展示。

arrays重复

IAP是数组的一个可见特征(通过current的函数系列公开),就像IAP计数在copy-on-write语义下的修改一样。 这不幸意味着foreach在许多情况下被迫重复它正在迭代的数组。 确切的条件是:

  1. 该数组不是一个引用(is_ref = 0)。 如果它是一个引用,那么它的改变应该传播,所以它不应该被复制。
  2. 该数组具有refcount> 1。 如果refcount是1,那么该数组不是共享的,我们可以直接修改它。

如果数组不重复(is_ref = 0,refcount = 1),那么只有它的引用计数会增加(*)。 此外,如果使用foreach引用,那么(可能重复的)数组将被转换为引用。

考虑这个代码作为发生重复的例子:

 function iterate($arr) { foreach ($arr as $v) {} } $outerArr = [0, 1, 2, 3, 4]; iterate($arr); 

在这里, $arr将被复制,以防止$arr上的IAP更改泄漏到$outerArr 。 就上面的条件而言,数组不是一个引用(is_ref = 0),在两个地方使用(refcount = 2)。 这个要求是不幸的,也是次最优实现的人造物(这里没有关于修改的问题,所以我们并不需要首先使用IAP)。

(*)在这里增加refcount听起来无害,但是违反了写时复制(COW)的语义:这意味着我们要修改refcount = 2数组的IAP,而COW规定修改只能在refcount上执行= 1个值。 这种违反会导致用户可见的行为更改(而COW通常是透明的),因为迭代数组上的IAP更改将是可观察的 – 但是直到数组上的第一个非IAP修改。 相反,这三个“有效的”选项应该是a)始终复制,b)不增加refcount,从而允许在循环中任意修改迭代数组,或者c)完全不使用IAP PHP 7解决scheme)。

位置推进顺序

有一个最后的实现细节,你必须注意正确理解下面的代码示例。 循环某些数据结构的“正常”方式在伪代码中看起来像这样:

 reset(arr); while (get_current_data(arr, &data) == SUCCESS) { code(); move_forward(arr); } 

不过,作为一个相当特殊的雪花,select稍微不同的做法:

 reset(arr); while (get_current_data(arr, &data) == SUCCESS) { move_forward(arr); code(); } 

也就是说,在循环体运行之前 ,数组指针已经向前移动了。 这意味着当循环体在元素$i上工作时,IAP已经在元素$i+1 。 这就是为什么在迭代期间显示修改的代码示例总是会取消下一个元素,而不是当前元素。

例子:你的testing用例

上述三个方面应该为您提供对每个实现的特质的完整印象,我们可以继续讨论一些例子。

testing用例的行为在这一点上很容易解释:

  • 在testing用例1和2 $array ,以refcount = 1开头,所以不会被foreach复制:只有refcount递增。 当循环体随后修改数组(在那一点refcount = 2)时,复制将在该点发生。 Foreach将继续处理$array的未修改副本。

  • 在testing用例3中,数组再次不被复制,因此foreach将修改$arrayvariables的IAP。 在迭代结束时,IAP为NULL(意味着迭代完成), each指示返回false

  • 在testing用例4和5中, each都是引用函数。 $array传递给它们时有一个refcount=2 ,所以它必须被复制。 因为这样的foreach将再次在一个单独的数组上工作。

示例: current在foreach中的效果

显示各种复制行为的一个好方法是观察foreach循环内current()函数的行为。 考虑这个例子:

 foreach ($array as $val) { var_dump(current($array)); } /* Output: 2 2 2 2 2 */ 

在这里你应该知道current()是一个by-ref函数(实际上是:prefer-ref),即使它不修改数组。 它必须是为了与所有其他function,如下所有这些都是by-ref打好。 通过引用传递意味着数组必须分开,因此$array和foreach数组将不同。 你得到2而不是1的原因也在上面提到: foreach 运行用户代码之前推进数组指针,而不是在之后。 所以即使代码在第一个元素,foreach已经提前指针到第二个。

现在让我们尝试一个小的修改:

 $ref = &$array; foreach ($array as $val) { var_dump(current($array)); } /* Output: 2 3 4 5 false */ 

这里我们有is_ref = 1的情况,所以数组不会被复制(就像上面一样)。 但是现在它是一个引用,当传递给by-ref current()函数时,数组不必再被复制。 因此current()和foreach在同一个数组上工作。 尽pipe如此,由于foreach提前指针的方式,你仍然可以看到一个一个的行为。

在执行by-ref迭代时会得到相同的行为:

 foreach ($array as &$val) { var_dump(current($array)); } /* Output: 2 3 4 5 false */ 

这里重要的一点是,foreach将通过引用迭代$array一个is_ref = 1,所以基本上你的情况和上面一样。

另一个小的变化,这次我们将数组分配给另一个variables:

 $foo = $array; foreach ($array as $val) { var_dump(current($array)); } /* Output: 1 1 1 1 1 */ 

这里$array循环开始的时候$array的refcount是2,所以一次我们实际上不得不先做重复。 因此, $array和foreach使用的数组将从一开始就完全分离。 这就是为什么你在循环之前获得IAP的位置(在这种情况下,它是在第一个位置)。

示例:迭代过程中的修改

在迭代期间试图解释修改是我们所有的foreach问题的起源,所以它考虑一些这个案例的例子。

考虑在同一个数组上的这些嵌套循环(其中使用by-ref迭代来确保它确实是相同的):

 foreach ($array as &$v1) { foreach ($array as &$v2) { if ($v1 == 1 && $v2 == 1) { unset($array[1]); } echo "($v1, $v2)\n"; } } // Output: (1, 1) (1, 3) (1, 4) (1, 5) 

这里期望的部分是(1, 2)从输出中丢失,因为元素1被删除。 可能出乎意料的是外层循环在第一个元素之后停止。 这是为什么?

其原因是上面描述的嵌套循环hack:在循环体运行之前,当前的IAP位置和散列被备份到HashPointer 。 在循环体之后,它将被恢复,但是只有当元素仍然存在时,才会使用当前的IAP位置(不pipe它是什么)。 在上面的例子中,情况正是如此:外层循环的当前元素已被删除,所以它将使用已经被内层循环标记为已完成的IAP!

HashPointer备份+恢复机制的另一个结果是,通过reset()等对IAP的更改通常不会影响foreach。 例如,下面的代码执行就好像reset()根本不存在:

 $array = [1, 2, 3, 4, 5]; foreach ($array as &$value) { var_dump($value); reset($array); } // output: 1, 2, 3, 4, 5 

原因是,虽然reset()暂时修改了IAP,但是它会被恢复到循环体之后的当前foreach元素。 要强制reset()在循环中生效,必须另外删除当前元素,以便备份/恢复机制失败:

 $array = [1, 2, 3, 4, 5]; $ref =& $array; foreach ($array as $value) { var_dump($value); unset($array[1]); reset($array); } // output: 1, 1, 3, 4, 5 

但是,这些例子仍然是理智的。 如果您记得HashPointer恢复使用指向元素的指针及其哈希来确定它是否仍然存在,那么真正的乐趣就开始了。 但是:哈希有碰撞,指针可以重用! 这意味着,通过仔细select数组键,我们可以让foreach相信已经被删除的元素仍然存在,所以它会直接跳转到它。 一个例子:

 $array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3]; $ref =& $array; foreach ($array as $value) { unset($array['EzFY']); $array['FYFY'] = 4; reset($array); var_dump($value); } // output: 1, 4 

在这里,我们通常应该按照以前的规则预期输出1,3,4。 怎么会发生这样的情况: 'FYFY'与被删除的元素'FYFY'具有相同的哈希,而分配器恰好重复使用相同的内存位置来存储元素。 所以foreach直接跳到新插入的元素,从而缩短了循环。

在循环中replace迭代的实体

我想提到的最后一个奇怪的情况是,PHP允许您在循环中replace迭代的实体。 所以你可以开始迭代一个数组,然后在另一个数组中replace它。 或者开始迭代数组,然后用一个对象replace它:

 $arr = [1, 2, 3, 4, 5]; $obj = (object) [6, 7, 8, 9, 10]; $ref =& $arr; foreach ($ref as $val) { echo "$val\n"; if ($val == 3) { $ref = $obj; } } /* Output: 1 2 3 6 7 8 9 10 */ 

正如你所看到的,在这种情况下,一旦replace发生,PHP将从头开始迭代另一个实体。

PHP 7

散列表迭代器

如果你还记得,数组迭代的主要问题是如何在迭代中处理元素的去除。 PHP 5为此使用了一个单一的内部数组指针(IAP),这是不太理想的,因为一个数组指针必须被拉伸以支持多个同时的foreach循环以及reset()等的交互。

PHP 7使用不同的方法,即支持创build任意数量的外部安全哈希表迭代器。 这些迭代器必须在数组中注册,从这一点上它们具有与IAP相同的语义:如果删除数组元素,则指向该元素的所有哈希表迭代器将前进到下一个元素。

这意味着foreach将不再使用IAP。 foreach循环对current()等的结果绝对没有影响,它的行为也不会受到像reset()等函数的影响。

arrays重复

PHP 5和PHP 7之间的另一个重要变化是与arrays重复有关。 现在不再使用IAP,在所有情况下,按值数组迭代只会执行一个refcount increment(而不是重复数组)。 如果数组在foreach循环期间被修改,那么会发生重复(根据copy-on-write),foreach将继续在旧数组上工作。

在大多数情况下,这种变化是透明的,除了更好的性能之外没有其他的作用 然而,有一种情况会导致不同的行为,也就是数组事先被引用的情况:

 $array = [1, 2, 3, 4, 5]; $ref = &$array; foreach ($array as $val) { var_dump($val); $array[2] = 0; } /* Old output: 1, 2, 0, 4, 5 */ /* New output: 1, 2, 3, 4, 5 */ 

之前的参考数组的值迭代是特殊情况。 在这种情况下,不会发生重复,所以在迭代过程中所有的数组修改都会被循环所反映。 在PHP 7中,这种特殊情况已经不存在了:数组的一个按值迭代将始终在原始元素上继续工作,而不pipe循环中的任何修改。

这当然不适用于参考迭代。 如果你通过引用迭代所有的修改将被循环反映。 有趣的是,对于普通对象的值迭代也是如此:

 $obj = new stdClass; $obj->foo = 1; $obj->bar = 2; foreach ($obj as $val) { var_dump($val); $obj->bar = 42; } /* Old and new output: 1, 42 */ 

这反映了对象的逐句柄语义(即,即使在按值的上下文中它们也performance为引用类似的)。

例子

让我们考虑几个例子,从你的testing用例开始:

  • testing用例1和2保留了相同的输出:按值数组迭代总是在原始元素上继续工作。 (在这种情况下,甚至在PHP 5和PHP 7之间的重复计数和重复行为也完全相同)。

  • testing用例3更改:Foreach不再使用IAP,因此each()不受循环的影响。 它会有相同的输出前后。

  • testing用例4和5保持不变:在更改IAP之前, each()reset()将复制数组,而foreach仍使用原始数组。 (并不是说IAP的变化是重要的,即使数组是共享的也是如此)

第二组示例与不同参考/计数configuration下current()的行为有关。 这不再有意义,因为current()完全不受循环影响,所以它的返回值始终保持不变。

但是,在迭代期间考虑修改时,我们会得到一些有趣的更改。 我希望你会发现新的行为更健全。 第一个例子:

 $array = [1, 2, 3, 4, 5]; foreach ($array as &$v1) { foreach ($array as &$v2) { if ($v1 == 1 && $v2 == 1) { unset($array[1]); } echo "($v1, $v2)\n"; } } // Old output: (1, 1) (1, 3) (1, 4) (1, 5) // New output: (1, 1) (1, 3) (1, 4) (1, 5) // (3, 1) (3, 3) (3, 4) (3, 5) // (4, 1) (4, 3) (4, 4) (4, 5) // (5, 1) (5, 3) (5, 4) (5, 5) 

正如你所看到的,外层循环不再在第一次迭代之后中止。 原因是两个循环现在都有完全独立的散列表迭代器,并且通过共享的IAP不再有任何交叉污染。

现在已经修复的另一个奇怪的边缘情况是,当您移除并添加碰巧具有相同散列的元素时,您会得到一个奇怪的效果:

 $array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3]; foreach ($array as &$value) { unset($array['EzFY']); $array['FYFY'] = 4; var_dump($value); } // Old output: 1, 4 // New output: 1, 3, 4 

以前,HashPointer恢复机制跳转到新元素,因为它看起来像移除元素一样(由于碰撞散列和指针)。 由于我们不再依赖元素散列来完成任何事情,这不再是一个问题。

在示例3中,不要修改数组。 在所有其他示例中,您可以修改内容或内部数组指针。 由于赋值运算符的语义,这对于PHP数组非常重要。

PHP中数组的赋值运算符更像是一个懒惰的克隆。 将一个variables赋值给包含数组的另一个variables将克隆该数组,这与大多数语言不同。 然而,除非需要,否则实际的克隆将不会完成。 这意味着克隆只有在任何一个variables被修改(写时复制)时才会发生。

这里是一个例子:

 $a = array(1,2,3); $b = $a; // This is lazy cloning of $a. For the time // being $a and $b point to the same internal // data structure. $a[] = 3; // Here $a changes, which triggers the actual // cloning. From now on, $a and $b are two // different data structures. The same would // happen if there were a change in $b. 

回想一下你的testing用例,你可以很容易想象foreach会创build一些带有对数组的引用的迭代器。 这个引用和我的例子中的variables$b完全一样。 但是,迭代器和引用一起只在循环中生效,然后它们都被丢弃。 现在你可以看到,除了3以外的所有情况下,数组在循环中被修改,而这个额外的引用是活着的。 这触发了一个克隆,这解释了这里发生了什么!

这是一个优秀的文章,这种写复制行为的另一个副作用: PHP三元运算符:快还是不?

使用foreach()时需要注意的几点:

a) foreach在原始数组的预期副本上工作。 这意味着foreach()将拥有SHARED数据存储空间,除非在Notes / User注释中没有创buildprospected copy

b)什么触发了预期的副本 ? 预期的副本是基于copy-on-writecopy-on-write的策略创build的,也就是说,只要传递给foreach()的数组发生更改,就会创build原始数组的副本。

c)原始数组和foreach()迭代器将具有DISTINCT SENTINEL VARIABLES ,即一个用于原始数组,另一个用于foreach; 请参阅下面的testing代码。 SPL , 迭代器和数组迭代器 。

堆栈溢出问题如何确保在PHP的'foreach'循环中重置值? 解决你的问题(3,4,5)。

以下示例显示每个()和reset()都不会影响foreach()迭代器的SENTINELvariables(for example, the current index variable)

 $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.net引用):

第一种forms在由array_expression给出的数组上循环。 在每次迭代时,当前元素的值被赋值为$ value,而内部数组指针被前进一个(所以在下一次迭代中,您将看到下一个元素)。

所以,在你的第一个例子中,你只有一个元素在数组中,当指针被移动下一个元素不存在,所以在添加新的元素foreach结束后,因为它已经“决定”它作为最后一个元素。

在你的第二个例子中,你从两个元素开始,foreach循环不在最后一个元素,所以它在下一次迭代中评估数组,因此意识到数组中有新的元素。

我相信这是所有的结果在文档中的每个迭代部分的解释,这可能意味着foreach在调用{}的代码之前完成所有的逻辑。

testing用例

如果你运行这个:

 <? $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”实际工作? which explains the internals behind this behaviour.

As per the documentation provided by PHP manual.

On each iteration, the value of the current element is assigned to $v and the internal
array pointer is advanced by one (so on the next iteration, you'll be looking at the next element).

So as per your first example:

 $array = ['foo'=>1]; foreach($array as $k=>&$v) { $array['bar']=2; echo($v); } 

$array have only single element, so as per the foreach execution, 1 assign to $v and it don't have any other element to move pointer

But in your second example:

 $array = ['foo'=>1, 'bar'=>2]; foreach($array as $k=>&$v) { $array['baz']=3; echo($v); } 

$array have two element, so now $array evaluate the zero indices and move the pointer by one. For first iteration of loop, added $array['baz']=3; as pass by reference.

PHP foreach loop can be used with Indexed arrays , Associative arrays and Object public variables .

In foreach loop, the first thing php does is that it creates a copy of the array which is to be iterated over. PHP then iterates over this new copy of the array rather than the original one. This is demonstrated in the below example:

 <?php $numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array echo '<pre>', print_r($numbers, true), '</pre>', '<hr />'; foreach($numbers as $index => $number){ $numbers[$index] = $number + 1; # this is making changes to the origial array echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # showing data from the copied array } echo '<hr />', '<pre>', print_r($numbers, true), '</pre>'; # shows the original values (also includes the newly added values). 

Besides this, php does allow to use iterated values as a reference to the original array value as well. This is demonstrated below:

 <?php $numbers = [1,2,3,4,5,6,7,8,9]; echo '<pre>', print_r($numbers, true), '</pre>'; foreach($numbers as $index => &$number){ ++$number; # we are incrementing the original value echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # this is showing the original value } echo '<hr />'; echo '<pre>', print_r($numbers, true), '</pre>'; # we are again showing the original value 

Note: It does not allow original array indexes to be used as references .

Source: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

Great question, because many developers, even experienced ones, are confused by the way PHP handles arrays in foreach loops. In the standard foreach loop, PHP makes a copy of the array that is used in the loop. The copy is discarded immediately after the loop finishes. This is transparent in the operation of a simple foreach loop. 例如:

 $set = array("apple", "banana", "coconut"); foreach ( $set AS $item ) { echo "{$item}\n"; } 

这输出:

 apple banana coconut 

So the copy is created but the developer doesn't notice, because the original array isn't referenced within the loop or after the loop finishes. However, when you attempt to modify the items in a loop, you find that they are unmodified when you finish:

 $set = array("apple", "banana", "coconut"); foreach ( $set AS $item ) { $item = strrev ($item); } print_r($set); 

这输出:

 Array ( [0] => apple [1] => banana [2] => coconut ) 

Any changes from the original can't be notices, actually there are no changes from the original, even though you clearly assigned a value to $item. This is because you are operating on $item as it appears in the copy of $set being worked on. You can override this by grabbing $item by reference, like so:

 $set = array("apple", "banana", "coconut"); foreach ( $set AS &$item ) { $item = strrev($item); } print_r($set); 

这输出:

 Array ( [0] => elppa [1] => ananab [2] => tunococ ) 

So it is evident and observable, when $item is operated on by-reference, the changes made to $item are made to the members of the original $set. Using $item by reference also prevents PHP from creating the array copy. To test this, first we'll show a quick script demonstrating the copy:

 $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 ) 

As it is shown in the example, PHP copied $set and used it to loop over, but when $set was used inside the loop, PHP added the variables to the original array, not the copied array. Basically, PHP is only using the copied array for the execution of the loop and the assignment of $item. Because of this, the loop above only executes 3 times, and each time it appends another value to the end of the original $set, leaving the original $set with 6 elements, but never entering an infinite loop.

However, what if we had used $item by reference, as I mentioned before? A single character added to the above test:

 $set = array("apple", "banana", "coconut"); foreach ( $set AS &$item ) { $set[] = ucfirst($item); } print_r($set); 

Results in an infinite loop. Note this actually is an infinite loop, you'll have to either kill the script yourself or wait for your OS to run out of memory. I added the following line to my script so PHP would run out of memory very quickly, I suggest you do the same if you're going to be running these infinite loop tests:

 ini_set("memory_limit","1M"); 

So in this previous example with the infinite loop, we see the reason why PHP was written to create a copy of the array to loop over. When a copy is created and used only by the structure of the loop construct itself, the array stays static throughout the execution of the loop, so you'll never run into issues.