PHP'Foreach'实际上是如何工作的?

让我在前面说我知道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 loopvar_dump(each($array));
foreach ($array as $item) {echo "$item\n";}
var_dump(each($array));
/* Outputarray(4) {[1]=>int(1)["value"]=>int(1)[0]=>int(0)["key"]=>int(0)}12345bool(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()等)的函数是否会影响循环的结果?
449018 次浏览

在示例3中,您不修改数组。在所有其他示例中,您修改内容或内部数组指针。由于赋值操作符的语义学,这对于php数组很重要。

PHP中数组的赋值操作符更像是一个懒惰的克隆。将一个变量分配给另一个包含数组的变量将克隆该数组,这与大多数语言不同。但是,除非需要,否则不会进行实际的克隆。这意味着只有当其中一个变量被修改时(写入时复制),克隆才会发生。

下面是一个例子:

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

回到你的测试用例,你可以很容易地想象foreach创建了某种带有数组引用的迭代器。这个引用的工作方式与我例子中的变量$b完全一样。然而,迭代器和引用只在循环期间存在,然后,它们都被丢弃了。现在你可以看到,除了3之外,在所有情况下,数组都在循环期间被修改,而这个额外的引用是活的。这触发了一个克隆,这解释了这里发生的事情!

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

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

a)foreach适用于原始数组的预期副本。这意味着foreach()将具有共享数据存储,直到或除非prospected copy是未创建Foreach笔记/用户评论

b)什么触发预期副本?基于copy-on-write的策略创建预期副本,即无论何时传递给foreach()的数组被更改,原始数组的克隆被创建。

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

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

下面的示例显示each()和重置()不影响SENTINEL变量foreach()迭代器的(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 => 1foreach: 0 => 1each() Original(inside): 1 => 2--------Iteration--------foreach: 1 => 2each() Original(inside): 2 => 3--------Iteration--------foreach: 2 => 3each() Original(inside): 3 => 4--------Iteration--------foreach: 3 => 4each() Original(inside): 4 => 5--------Iteration--------Resetting original array pointerforeach: 4 => 5each() Original(inside): 0=>1--------Iteration--------each() Original (outside): 1 => 2

foreach支持对三种不同类型的值进行迭代:

  • 数组
  • 普通物体
  • #0对象

在下面,我将尝试准确解释迭代在不同情况下是如何工作的。到目前为止,最简单的情况是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();/* ... */}

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

数组和普通对象的迭代要复杂得多。首先,应该注意的是,在PHP中,“数组”实际上是有序的字典,它们将根据这个顺序进行遍历(只要你不使用sort之类的东西,它就会匹配插入顺序)。这与按键的自然顺序迭代(其他语言中的列表通常是如何工作的)或根本没有定义的顺序(其他语言中的字典通常是如何工作的)相反。

这也适用于对象,因为对象属性可以被视为另一个(有序)字典,将属性名称映射到它们的值,加上一些可见性处理。在大多数情况下,对象属性实际上并没有以这种相当低效的方式存储。然而,如果你开始迭代一个对象,通常使用的打包表示将被转换为一个真正的字典。在这一点上,普通对象的迭代变得非常类似于数组的迭代(这就是为什么我在这里不太讨论普通对象迭代)。

到目前为止,一切顺利。迭代字典不会太难,对吧?当你意识到数组/对象可以在迭代过程中发生变化时,问题就开始了。这有多种可能发生的方式:

  • 如果您使用foreach ($arr as &$v)按引用迭代,则$arr将转换为引用,您可以在迭代期间更改它。
  • 在PHP 5中,即使您按值迭代,也同样适用,但数组是事先的引用:$ref =& $arr; foreach ($ref as $v)
  • 对象具有句柄传递语义学,这对于大多数实际目的意味着它们的行为类似于引用。因此对象总是可以在迭代期间更改。

在迭代期间允许修改的问题是您当前所在的元素被删除的情况。假设您使用指针来跟踪您当前所在的数组元素。如果此元素现在被释放,您将留下一个悬空指针(通常会导致分段错误)。

解决这个问题有不同的方法。PHP 5和PHP 7在这方面有很大的不同,我将在下面描述这两种行为。总结是,PHP 5的方法相当愚蠢,导致各种奇怪的边缘情况问题,而PHP 7的更多参与的方法导致更可预测和一致的行为。

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

php5

内部数组指针和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 copyforeach ($arr as &$v1) {foreach ($arr as &$v) {// ...}}

为了支持两个只有一个内部数组指针的同时循环,foreach执行以下逻辑:

数组复制

IAP是数组的一个可见特征(通过current系列函数公开),因为IAP的此类更改在写入时复制语义学下被视为修改。不幸的是,这意味着foreach在许多情况下被迫复制它正在迭代的数组。确切的条件是:

  1. 数组不是引用(is_ref=0)。如果它是引用,那么对它的更改将传播应该,因此不应复制它。
  2. 数组的引用计数>1。如果refcount为1,则数组不共享,我们可以直接修改它。

如果数组不重复(is_ref=0,refcount=1),则仅其refcount将递增(*)。此外,如果使用foreach by引用,则(可能重复的)数组将变为引用。

将此代码视为发生重复的示例:

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

在这里,$arr将被复制以防止$arr上的IAP更改泄漏到$outerArr。就上述条件而言,数组不是引用(is_ref=0)并且在两个地方使用(refcoun=2)。这个需求是不幸的,也是次优实现的产物(这里没有考虑迭代期间的修改,所以我们一开始就不需要使用IAP)。

(*)在此处递增refcount听起来无害,但违反了写入时复制(COW)语义学:这意味着我们将修改refcoun=2数组的IAP,而COW规定只能对refcoun=1值执行修改。这种违规会导致用户可见的行为变化(而COW通常是透明的),因为迭代数组上的IAP变化将是可观察的--但只有在数组上第一次非IAP修改之前。相反,三个“有效”选项应该是a)总是重复,b)不增加refcount,从而允许在循环中任意修改迭代数组,或者c)根本不使用IAP(PHP 7解决方案)。

职位提级顺序

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

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

然而foreach,作为一个相当特殊的雪花,选择做稍微不同的事情:

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

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

示例:您的测试用例

上面描述的三个方面应该为您提供对foreach实现特性的大致完整的印象,我们可以继续讨论一些示例。

在这一点上,您的测试用例的行为很容易解释:

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

  • 在测试用例3中,数组再次不重复,因此foreach将修改$array变量的IAP。在迭代结束时,IAP为NULL(意味着迭代已经完成),each通过返回false表示。

  • 在测试用例4和5中,eachreset都是通过引用函数。$array在传递给它们时有一个refcount=2,因此必须复制它。因此foreach将再次处理单独的数组。

示例:Foreach中current的影响

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

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

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

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

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

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

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

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

这里重要的部分是Foreach在通过引用迭代时会使$arrayis_ref=1,所以基本上你有和上面一样的情况。

另一个小变体,这次我们将数组分配给另一个变量:

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

这里循环开始时$array的引用计数是2,所以这一次我们实际上必须预先复制。因此$array和foreach使用的数组将从一开始就完全分开。这就是为什么你会得到IAP在循环之前的位置(在这种情况下,它在第一个位置)。

示例:迭代期间的修改

试图在迭代过程中解释修改是我们所有这些问题的起源,所以它有助于考虑这种情况的一些例子。

考虑相同数组上的这些嵌套循环(其中使用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已被删除。可能出乎意料的是,外部循环在第一个元素之后停止。这是为什么?

这背后的原因是上面描述的嵌套循环黑客:在循环主体运行之前,当前IAP位置和哈希被备份到HashPointer中。在循环主体之后,它将被恢复,但前提是元素仍然存在,否则将使用当前IAP位置(无论它可能是什么)。在上面的例子中,情况正是如此:外部循环的当前元素已被删除,因此它将使用IAP,该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还原使用指向元素及其哈希的指针来确定它是否仍然存在。但是:哈希有冲突,指针可以重用!这意味着,通过仔细选择数组键,我们可以让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, 1, 3, 4。发生的情况是'FYFY'与删除的元素'EzFY'具有相同的哈希值,并且分配器碰巧重用相同的内存位置来存储元素。因此foreach最终直接跳转到新插入的元素,从而缩短循环。

在循环期间替换迭代实体

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

$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 */

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

php7

哈希表迭代器

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

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

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

数组复制

PHP 5和PHP 7之间的另一个重要变化与数组复制有关。现在不再使用IAP,在所有情况下,按值数组迭代只会进行refcount增量(而不是复制数组)。如果数组在foreach循环期间被修改,此时将发生复制(根据写入时复制),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中,这种特殊情况消失了:数组的按值迭代将总是继续处理原始元素,而不管循环期间的任何修改。

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

$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 */

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

示例

让我们考虑几个例子,从您的测试用例开始:

  • 测试用例1和2保留相同的输出:按值数组迭代始终保持对原始元素的工作。(在这种情况下,即使refcounting和复制行为在PHP 5和PHP 7之间也完全相同)。

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

  • 测试用例4和5保持不变:each()reset()将在更改IAP之前复制数组,而foreach仍然使用原始数组。(即使数组是共享的,IAP更改也无关紧要。)

第二组示例与current()在不同reference/refcounting配置下的行为有关。这不再有意义,因为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恢复机制直接跳转到新元素,因为它“看起来”像是与删除的元素相同(由于哈希和指针的碰撞)。由于我们不再依赖元素哈希做任何事情,这不再是一个问题。

PHP 7的注意事项

更新这个答案,因为它已经获得了一些流行:这个答案从PHP 7开始不再适用。正如“向后不兼容的更改”中解释的那样,在PHP 7中,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'实际上是如何工作的?中阅读,它解释了这种行为背后的内部。

根据PHP手册提供的留档。

在每次迭代中,当前元素的值被分配给$v和内部
数组指针前进1(因此在下一次迭代中,您将查看下一个元素)。

根据你的第一个例子:

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

$array只有一个元素,所以根据foreach执行,1分配给$v,它没有任何其他元素来移动指针

在你的第二个例子中:

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

$array有两个元素,所以现在$数组评估零索引并将指针移动一。对于循环的第一次迭代,添加$array['baz']=3;作为引用传递。

很好的问题,因为许多开发人员,甚至是有经验的开发人员,都对PHP在Foreach循环中处理数组的方式感到困惑。在标准的Foreach循环中,PHP复制循环中使用的数组。循环完成后立即丢弃该副本。这在简单的Foreach循环的操作中是透明的。例如:

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

此输出:

applebananacoconut

所以副本被创建了,但开发人员没有注意到,因为原始数组在循环中或循环完成后没有被引用。然而,当你尝试修改循环中的项目时,你会发现当你完成时它们未被修改:

$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所做的更改是对原始$集的成员进行的,这是显而易见的和可观察的。通过引用使用$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只使用复制的数组来执行循环和$项目的赋值。正因为如此,上面的循环只执行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以创建要循环的数组副本的原因。当副本仅由循环构造本身的结构创建和使用时,数组在整个循环执行过程中保持静态,因此您永远不会遇到问题。

PHP Foreach循环可以与Indexed arraysAssociative arraysObject public variables一起使用。

在Foreach循环中,php做的第一件事是创建要迭代的数组的副本。然后PHP迭代这个新的数组copy而不是原始数组。这在下面的示例中演示:

<?php$numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our arrayecho '<pre>', print_r($numbers, true), '</pre>', '<hr />';foreach($numbers as $index => $number){$numbers[$index] = $number + 1; # this is making changes to the origial arrayecho '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).

除此之外,php也允许使用iterated values as a reference to the original array value。如下所示:

<?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 valueecho '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

备注:它不允许将original array indexes用作references

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