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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10057671/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 21:17:58  来源:igfitidea点击:

How does PHP 'foreach' actually work?

phploopsforeachiterationphp-internals

提问by DaveRandom

Let me prefix this by saying that I know what foreachis, does and how to use it. This question concerns how it works under the bonnet, and I don't want any answers along the lines of "this is how you loop an array with foreach".

让我先说我知道它是什么foreach、做什么以及如何使用它。这个问题涉及它在引擎盖下是如何工作的,我不想要任何类似“这就是你如何使用foreach”循环数组的答案。



For a long time I assumed that foreachworked with the array itself. Then I found many references to the fact that it works with a copyof the array, and I have since assumed this to be the end of the story. But I recently got into a discussion on the matter, and after a little experimentation found that this was not in fact 100% true.

很长一段时间内,我都认为它foreach与数组本身一起工作。然后我发现了很多关于它与数组副本一起工作的事实的参考,并且我从那时起假设这就是故事的结尾。但是我最近就此事进行了讨论,经过一些实验后发现这实际上并非 100% 正确。

Let me show what I mean. For the following test cases, we will be working with the following array:

让我展示一下我的意思。对于以下测试用例,我们将使用以下数组:

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

Test case 1:

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

This clearly shows that we are not working directly with the source array - otherwise the loop would continue forever, since we are constantly pushing items onto the array during the loop. But just to be sure this is the case:

这清楚地表明我们没有直接使用源数组 - 否则循环将永远持续下去,因为我们在循环期间不断将项目推入数组。但为了确定情况确实如此:

Test case 2:

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

This backs up our initial conclusion, we are working with a copy of the source array during the loop, otherwise we would see the modified values during the loop. But...

这支持了我们最初的结论,我们在循环期间使用源数组的副本,否则我们将在循环期间看到修改后的值。但...

If we look in the manual, we find this statement:

如果我们查看手册,我们会发现以下语句:

When foreach first starts executing, the internal array pointer is automatically reset to the first element of the array.

当 foreach 首次开始执行时,内部数组指针会自动重置为数组的第一个元素。

Right... this seems to suggest that foreachrelies on the array pointer of the source array. But we've just proved that we're not working with the source array, right? Well, not entirely.

对...这似乎表明foreach依赖于源数组的数组指针。但我们刚刚证明我们没有使用源数组,对吗?嗯,不完全是。

Test case 3:

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

So, despite the fact that we are not working directly with the source array, we are working directly with the source array pointer - the fact that the pointer is at the end of the array at the end of the loop shows this. Except this can't be true - if it was, then test case 1would loop forever.

因此,尽管我们不直接使用源数组,但我们直接使用源数组指针 - 指针位于循环末尾的数组末尾的事实表明了这一点。除了这不可能是真的 - 如果是这样,那么测试用例 1将永远循环。

The PHP manual also states:

PHP 手册还指出:

As foreach relies on the internal array pointer changing it within the loop may lead to unexpected behavior.

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

Well, let's find out what that "unexpected behavior" is (technically, any behavior is unexpected since I no longer know what to expect).

好吧,让我们找出“意外行为”是什么(从技术上讲,任何行为都是意外的,因为我不再知道会发生什么)。

Test case 4:

测试案例 4

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

Test case 5:

测试用例 5

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

...nothing that unexpected there, in fact it seems to support the "copy of source" theory.

...没有什么出乎意料的,实际上它似乎支持“来源副本”理论。



The Question

问题

What is going on here? My C-fu is not good enough for me to able to extract a proper conclusion simply by looking at the PHP source code, I would appreciate it if someone could translate it into English for me.

这里发生了什么?我的 C-fu 不够好,我无法仅通过查看 PHP 源代码来提取正确的结论,如果有人能帮我将其翻译成英文,我将不胜感激。

It seems to me that foreachworks with a copyof the array, but sets the array pointer of the source array to the end of the array after the loop.

在我看来,它foreach适用于数组的副本,但在循环后将源数组的数组指针设置为数组的末尾。

  • Is this correct and the whole story?
  • If not, what is it really doing?
  • Is there any situation where using functions that adjust the array pointer (each(), reset()et al.) during a foreachcould affect the outcome of the loop?
  • 这是正确的和整个故事吗?
  • 如果不是,它真正在做什么?
  • 是否有一个地方使用调整数组指针函数(任何情况下each()reset()等)过程中foreach可能会影响循环的结果?

采纳答案by NikiC

foreachsupports iteration over three different kinds of values:

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

In the following, I will try to explain precisely how iteration works in different cases. By far the simplest case is Traversableobjects, as for these foreachis essentially only syntax sugar for code along these lines:

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

For internal classes, actual method calls are avoided by using an internal API that essentially just mirrors the Iteratorinterface on the C level.

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

Iteration of arrays and plain objects is significantly more complicated. First of all, it should be noted that in PHP "arrays" are really ordered dictionaries and they will be traversed according to this order (which matches the insertion order as long as you didn't use something like sort). This is opposed to iterating by the natural order of the keys (how lists in other languages often work) or having no defined order at all (how dictionaries in other languages often work).

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

The same also applies to objects, as the object properties can be seen as another (ordered) dictionary mapping property names to their values, plus some visibility handling. In the majority of cases, the object properties are not actually stored in this rather inefficient way. However, if you start iterating over an object, the packed representation that is normally used will be converted to a real dictionary. At that point, iteration of plain objects becomes very similar to iteration of arrays (which is why I'm not discussing plain-object iteration much in here).

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

So far, so good. Iterating over a dictionary can't be too hard, right? The problems begin when you realize that an array/object can change during iteration. There are multiple ways this can happen:

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

  • If you iterate by reference using foreach ($arr as &$v)then $arris turned into a reference and you can change it during iteration.
  • In PHP 5 the same applies even if you iterate by value, but the array was a reference beforehand: $ref =& $arr; foreach ($ref as $v)
  • Objects have by-handle passing semantics, which for most practical purposes means that they behave like references. So objects can always be changed during iteration.
  • 如果您通过引用进行迭代使用foreach ($arr as &$v)then$arr变成了一个引用,您可以在迭代期间更改它。
  • 在 PHP 5 中,即使您按值迭代也是如此,但该数组是事先引用的: $ref =& $arr; foreach ($ref as $v)
  • 对象具有通过句柄传递的语义,对于大多数实际目的而言,这意味着它们的行为类似于引用。所以对象在迭代过程中总是可以改变的。

The problem with allowing modifications during iteration is the case where the element you are currently on is removed. Say you use a pointer to keep track of which array element you are currently at. If this element is now freed, you are left with a dangling pointer (usually resulting in a segfault).

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

There are different ways of solving this issue. PHP 5 and PHP 7 differ significantly in this regard and I'll describe both behaviors in the following. The summary is that PHP 5's approach was rather dumb and lead to all kinds of weird edge-case issues, while PHP 7's more involved approach results in more predictable and consistent behavior.

有不同的方法来解决这个问题。PHP 5 和 PHP 7 在这方面有很大的不同,我将在下面描述这两种行为。总而言之,PHP 5 的方法相当愚蠢,会导致各种奇怪的边缘问题,而 PHP 7 更复杂的方法导致更可预测和一致的行为。

As a last preliminary, it should be noted that PHP uses reference counting and copy-on-write to manage memory. This means that if you "copy" a value, you actually just reuse the old value and increment its reference count (refcount). Only once you perform some kind of modification a real copy (called a "duplication") will be done. See You're being lied tofor a more extensive introduction on this topic.

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

PHP 5

PHP 5

Internal array pointer and HashPointer

内部数组指针和HashPointer

Arrays in PHP 5 have one dedicated "internal array pointer" (IAP), which properly supports modifications: Whenever an element is removed, there will be a check whether the IAP points to this element. If it does, it is advanced to the next element instead.

PHP 5 中的数组有一个专用的“内部数组指针”(IAP),它正确地支持修改:每当删除一个元素时,都会检查 IAP 是否指向这个元素。如果是,则改为前进到下一个元素。

While foreachdoes make use of the IAP, there is an additional complication: There is only one IAP, but one array can be part of multiple foreachloops:

虽然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) {
        // ...
    }
}

To support two simultaneous loops with only one internal array pointer, foreachperforms the following shenanigans: Before the loop body is executed, foreachwill back up a pointer to the current element and its hash into a per-foreach HashPointer. After the loop body runs, the IAP will be set back to this element if it still exists. If however the element has been removed, we'll just use wherever the IAP is currently at. This scheme mostly-kinda-sort of works, but there's a lot of weird behavior you can get out of it, some of which I'll demonstrate below.

为了支持只有一个内部数组指针的两个同时循环,foreach执行以下诡计:在执行循环体之前,foreach将一个指向当前元素的指针及其散列备份到 per-foreach 中HashPointer。循环体运行后,如果它仍然存在,IAP 将被设置回该元素。但是,如果该元素已被删除,我们将只使用 IAP 当前所在的位置。这个方案基本上有点工作,但是你可以摆脱很多奇怪的行为,我将在下面演示其中的一些。

Array duplication

数组复制

The IAP is a visible feature of an array (exposed through the currentfamily of functions), as such changes to the IAP count as modifications under copy-on-write semantics. This, unfortunately, means that foreachis in many cases forced to duplicate the array it is iterating over. The precise conditions are:

IAP 是数组的一个可见特性(通过current函数系列公开),因为在写时复制语义下,对 IAP 的更改算作修改。不幸的是,这意味着foreach在许多情况下被迫复制它正在迭代的数组。具体条件是:

  1. The array is not a reference (is_ref=0). If it's a reference, then changes to it are supposedto propagate, so it should not be duplicated.
  2. The array has refcount>1. If refcountis 1, then the array is not shared and we're free to modify it directly.
  1. 该数组不是引用 (is_ref=0)。如果它是一个引用,那么对它的更改应该会传播,所以它不应该被复制。
  2. 该数组的引用计数> 1。如果refcount是 1,则数组不共享,我们可以直接修改它。

If the array is not duplicated (is_ref=0, refcount=1), then only its refcountwill be incremented (*). Additionally, if foreachby reference is used, then the (potentially duplicated) array will be turned into a reference.

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

Consider this code as an example where duplication occurs:

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

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

Here, $arrwill be duplicated to prevent IAP changes on $arrfrom leaking to $outerArr. In terms of the conditions above, the array is not a reference (is_ref=0) and is used in two places (refcount=2). This requirement is unfortunate and an artifact of the suboptimal implementation (there is no concern of modification during iteration here, so we don't really need to use the IAP in the first place).

在这里,$arr将被复制以防止 IAP 更改$arr泄漏到$outerArr. 就上述条件而言,数组不是引用(is_ref=0),而是用在两个地方(refcount=2)。这个要求是不幸的,并且是次优实现的产物(这里没有迭代过程中的修改问题,所以我们实际上不需要首先使用 IAP)。

(*) Incrementing the refcounthere sounds innocuous, but violates copy-on-write (COW) semantics: This means that we are going to modify the IAP of a refcount=2 array, while COW dictates that modifications can only be performed on refcount=1 values. This violation results in user-visible behavior change (while a COW is normally transparent) because the IAP change on the iterated array will be observable -- but only until the first non-IAP modification on the array. Instead, the three "valid" options would have been a) to always duplicate, b) do not increment the refcountand thus allowing the iterated array to be arbitrarily modified in the loop or c) don't use the IAP at all (the PHP 7 solution).

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

Position advancement order

职位晋升顺序

There is one last implementation detail that you have to be aware of to properly understand the code samples below. The "normal" way of looping through some data structure would look something like this in pseudocode:

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

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

However foreach, being a rather special snowflake, chooses to do things slightly differently:

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

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

Namely, the array pointer is already moved forward beforethe loop body runs. This means that while the loop body is working on element $i, the IAP is already at element $i+1. This is the reason why code samples showing modification during iteration will always unsetthe nextelement, rather than the current one.

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

Examples: Your test cases

示例:您的测试用例

The three aspects described above should provide you with a mostly complete impression of the idiosyncrasies of the foreachimplementation and we can move on to discuss some examples.

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

The behavior of your test cases is simple to explain at this point:

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

  • In test cases 1 and 2 $arraystarts off with refcount=1, so it will not be duplicated by foreach: Only the refcountis incremented. When the loop body subsequently modifies the array (which has refcount=2 at that point), the duplication will occur at that point. Foreach will continue working on an unmodified copy of $array.

  • In test case 3, once again the array is not duplicated, thus foreachwill be modifying the IAP of the $arrayvariable. At the end of the iteration, the IAP is NULL (meaning iteration has done), which eachindicates by returning false.

  • In test cases 4 and 5 both eachand resetare by-reference functions. The $arrayhas a refcount=2when it is passed to them, so it has to be duplicated. As such foreachwill be working on a separate array again.

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

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

  • 在测试用例 4 和 5 中,eachreset都是按引用函数。该$array有一个refcount=2当它传递给他们,所以它必须被复制。因此foreach将再次处理单独的阵列。

Examples: Effects of currentin foreach

示例:currentin foreach 的效果

A good way to show the various duplication behaviors is to observe the behavior of the current()function inside a foreachloop. Consider this example:

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

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

Here you should know that current()is a by-ref function (actually: prefer-ref), even though it does not modify the array. It has to be in order to play nice with all the other functions like nextwhich are all by-ref. By-reference passing implies that the array has to be separated and thus $arrayand the foreach-arraywill be different. The reason you get 2instead of 1is also mentioned above: foreachadvances the array pointer beforerunning the user code, not after. So even though the code is at the first element, foreachalready advanced the pointer to the second.

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

Now lets try a small modification:

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

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

Here we have the is_ref=1 case, so the array is not copied (just like above). But now that it is a reference, the array no longer has to be duplicated when passing to the by-ref current()function. Thus current()and foreachwork on the same array. You still see the off-by-one behavior though, due to the way foreachadvances the pointer.

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

You get the same behavior when doing by-ref iteration:

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

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

Here the important part is that foreach will make $arrayan is_ref=1 when it is iterated by reference, so basically you have the same situation as above.

这里重要的部分是foreach$array在通过引用迭代时会产生一个is_ref=1,所以基本上你和上面的情况是一样的。

Another small variation, this time we'll assign the array to another variable:

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

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

Here the refcount of the $arrayis 2 when the loop is started, so for once we actually have to do the duplication upfront. Thus $arrayand the array used by foreach will be completely separate from the outset. That's why you get the position of the IAP wherever it was before the loop (in this case it was at the first position).

这里$array循环开始时的引用计数为 2,因此我们实际上必须预先进行复制。因此$array,foreach 使用的数组将从一开始就完全分开。这就是为什么您可以在循环之前的任何位置获取 IAP 的位置(在这种情况下,它位于第一个位置)。

Examples: Modification during iteration

示例:迭代期间的修改

Trying to account for modifications during iteration is where all our foreach troubles originated, so it serves to consider some examples for this case.

试图在迭代期间考虑修改是我们所有 foreach 问题的根源,因此它有助于考虑这种情况的一些示例。

Consider these nested loops over the same array (where by-ref iteration is used to make sure it really is the same one):

考虑在同一个数组上的这些嵌套循环(其中使用 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)

The expected part here is that (1, 2)is missing from the output because element 1was removed. What's probably unexpected is that the outer loop stops after the first element. Why is that?

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

The reason behind this is the nested-loop hack described above: Before the loop body runs, the current IAP position and hash is backed up into a HashPointer. After the loop body it will be restored, but only if the element still exists, otherwise the current IAP position (whatever it may be) is used instead. In the example above this is exactly the case: The current element of the outer loop has been removed, so it will use the IAP, which has already been marked as finished by the inner loop!

这背后的原因是上面描述的嵌套循环黑客:在循环体运行之前,当前的 IAP 位置和散列被备份到HashPointer. 在循环体之后,它将被恢复,但前提是元素仍然存在,否则将使用当前 IAP 位置(无论它是什么)。在上面的示例中,情况正是如此:外循环的当前元素已被删除,因此它将使用已被内循环标记为完成的 IAP!

Another consequence of the HashPointerbackup+restore mechanism is that changes to the IAP through reset()etc. usually do not impact foreach. For example, the following code executes as if the reset()were not present at all:

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

The reason is that, while reset()temporarily modifies the IAP, it will be restored to the current foreach element after the loop body. To force reset()to make an effect on the loop, you have to additionally remove the current element, so that the backup/restore mechanism fails:

原因是,在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

But, those examples are still sane. The real fun starts if you remember that the HashPointerrestore uses a pointer to the element and its hash to determine whether it still exists. But: Hashes have collisions, and pointers can be reused! This means that, with a careful choice of array keys, we can make foreachbelieve that an element that has been removed still exists, so it will jump directly to it. An example:

但是,这些例子仍然是理智的。如果您记得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

Here we should normally expect the output 1, 1, 3, 4according to the previous rules. How what happens is that 'FYFY'has the same hash as the removed element 'EzFY', and the allocator happens to reuse the same memory location to store the element. So foreach ends up directly jumping to the newly inserted element, thus short-cutting the loop.

在这里,我们通常应该1, 1, 3, 4根据之前的规则期待输出。发生的事情是它'FYFY'与删除的元素具有相同的哈希值'EzFY',并且分配器碰巧重用相同的内存位置来存储元素。所以 foreach 最终直接跳转到新插入的元素,从而缩短了循环。

Substituting the iterated entity during the loop

在循环期间替换迭代的实体

One last odd case that I'd like to mention, it is that PHP allows you to substitute the iterated entity during the loop. So you can start iterating on one array and then replace it with another array halfway through. Or start iterating on an array and then replace it with an object:

我想提到的最后一个奇怪的情况是,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 */

As you can see in this case PHP will just start iterating the other entity from the start once the substitution has happened.

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

PHP 7

PHP 7

Hashtable iterators

哈希表迭代器

If you still remember, the main problem with array iteration was how to handle removal of elements mid-iteration. PHP 5 used a single internal array pointer (IAP) for this purpose, which was somewhat suboptimal, as one array pointer had to be stretched to support multiple simultaneous foreach loops andinteraction with reset()etc. on top of that.

如果您还记得,数组迭代的主要问题是如何处理迭代中元素的移除。PHP 5 为此目的使用了单个内部数组指针 (IAP),这在某种程度上是次优的,因为必须拉伸一个数组指针以支持多个同时进行的 foreach 循环交互reset()等。

PHP 7 uses a different approach, namely, it supports creating an arbitrary amount of external, safe hashtable iterators. These iterators have to be registered in the array, from which point on they have the same semantics as the IAP: If an array element is removed, all hashtable iterators pointing to that element will be advanced to the next element.

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

This means that foreachwill no longer use the IAP at all. The foreachloop will be absolutely no effect on the results of current()etc. and its own behavior will never be influenced by functions like reset()etc.

这意味着foreach将不再使用IAP可言。该foreach循环都会对结果毫无影响current()等,以及其自己的行为绝不会由像功能的影响 reset()等。

Array duplication

数组复制

Another important change between PHP 5 and PHP 7 relates to array duplication. Now that the IAP is no longer used, by-value array iteration will only do a refcountincrement (instead of duplication the array) in all cases. If the array is modified during the foreachloop, at that point a duplication will occur (according to copy-on-write) and foreachwill keep working on the old array.

PHP 5 和 PHP 7 之间的另一个重要变化与数组重复有关。现在不再使用 IAP,refcount在所有情况下,按值数组迭代只会执行增量(而不是复制数组)。如果在foreach循环过程中修改了数组,那么将发生重复(根据写时复制),foreach并将继续处理旧数组。

In most cases, this change is transparent and has no other effect than better performance. However, there is one occasion where it results in different behavior, namely the case where the array was a reference beforehand:

在大多数情况下,这种变化是透明的,除了更好的性能之外没有其他影响。但是,有一种情况会导致不同的行为,即数组事先是引用的情况:

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

Previously by-value iteration of reference-arrays was special cases. In this case, no duplication occurred, so all modifications of the array during iteration would be reflected by the loop. In PHP 7 this special case is gone: A by-value iteration of an array will alwayskeep working on the original elements, disregarding any modifications during the loop.

以前,引用数组的按值迭代是特殊情况。在这种情况下,没有发生重复,因此迭代期间对数组的所有修改都将反映在循环中。在 PHP 7 中,这种特殊情况消失了:数组的按值迭代将始终处理原始元素,而忽略循环期间的任何修改。

This, of course, does not apply to by-reference iteration. If you iterate by-reference all modifications will be reflected by the loop. Interestingly, the same is true for by-value iteration of plain objects:

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

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

This reflects the by-handle semantics of objects (i.e. they behave reference-like even in by-value contexts).

这反映了对象的 by-handle 语义(即,即使在 by-value 上下文中,它们的行为也类似于引用)。

Examples

例子

Let's consider a few examples, starting with your test cases:

让我们考虑一些示例,从您的测试用例开始:

  • Test cases 1 and 2 retain the same output: By-value array iteration always keep working on the original elements. (In this case, even refcountingand duplication behavior is exactly the same between PHP 5 and PHP 7).

  • Test case 3 changes: Foreachno longer uses the IAP, so each()is not affected by the loop. It will have the same output before and after.

  • Test cases 4 and 5 stay the same: each()and reset()will duplicate the array before changing the IAP, while foreachstill uses the original array. (Not that the IAP change would have mattered, even if the array was shared.)

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

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

  • 测试案例4和5保持不变:each()reset()将复制阵列改变IAP之前,而foreach仍然使用原来的阵列。(并不是说 IAP 更改会很重要,即使阵列是共享的。)

The second set of examples was related to the behavior of current()under different reference/refcountingconfigurations. This no longer makes sense, as current()is completely unaffected by the loop, so its return value always stays the same.

第二组示例与current()不同reference/refcounting配置下的行为有关。这不再有意义,因为current()完全不受循环影响,因此其返回值始终保持不变。

However, we get some interesting changes when considering modifications during iteration. I hope you will find the new behavior saner. The first example:

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

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

As you can see, the outer loop no longer aborts after the first iteration. The reason is that both loops now have entirely separate hashtable iterators, and there is no longer any cross-contamination of both loops through a shared IAP.

如您所见,外循环在第一次迭代后不再中止。原因是两个循环现在都有完全独立的哈希表迭代器,并且两个循环不再通过共享 IAP 进行交叉污染。

Another weird edge case that is fixed now, is the odd effect you get when you remove and add elements that happen to have the same hash:

现在修复的另一个奇怪的边缘情况是当您删除和添加碰巧具有相同哈希值的元素时得到的奇怪效果:

$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

Previously the HashPointer restore mechanism jumped right to the new element because it "looked" like it's the same as the removed element (due to colliding hash and pointer). As we no longer rely on the element hash for anything, this is no longer an issue.

以前 HashPointer 恢复机制直接跳转到新元素,因为它“看起来”与删除的元素相同(由于哈希和指针冲突)。由于我们不再依赖元素哈希来做任何事情,这不再是一个问题。

回答by linepogl

In example 3 you don't modify the array. In all other examples you modify either the contents or the internal array pointer. This is important when it comes to PHParrays because of the semantics of the assignment operator.

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

The assignment operator for the arrays in PHP works more like a lazy clone. Assigning one variable to another that contains an array will clone the array, unlike most languages. However, the actual cloning will not be done unless it is needed. This means that the clone will take place only when either of the variables is modified (copy-on-write).

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

Here is an example:

下面是一个例子:

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

Coming back to your test cases, you can easily imagine that foreachcreates some kind of iterator with a reference to the array. This reference works exactly like the variable $bin my example. However, the iterator along with the reference live only during the loop and then, they are both discarded. Now you can see that, in all cases but 3, the array is modified during the loop, while this extra reference is alive. This triggers a clone, and that explains what's going on here!

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

Here is an excellent article for another side effect of this copy-on-write behaviour: The PHP Ternary Operator: Fast or not?

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

回答by sakhunzai

Some points to note when working with foreach():

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

a) foreachworks on the prospected copyof the original array. It means foreach()will have SHARED data storage until or unless a prospected copyis not created foreach Notes/User comments.

a)处理原始数组foreach预期副本。这意味着foreach()将拥有 SHARED 数据存储,直到或除非prospected copy未为每个 Notes/User comments创建。

b) What triggers a prospected copy? A prospected copy is created based on the policy of copy-on-write, that is, whenever an array passed to foreach()is changed, a clone of the original array is created.

b) 什么触发了预期副本?预期副本是基于 的策略创建的copy-on-write,即,每当传递给的数组foreach()发生更改时,都会创建原始数组的克隆。

c) The original array and foreach()iterator will have DISTINCT SENTINEL VARIABLES, that is, one for the original array and other for foreach; see the test code below. SPL, Iterators, and Array Iterator.

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

Stack Overflow question How to make sure the value is reset in a 'foreach' loop in PHP?addresses the cases (3,4,5) of your question.

堆栈溢出问题如何确保在 PHP 的“foreach”循环中重置该值?解决您问题的案例 (3,4,5)。

The following example shows that each() and reset() DOES NOT affect SENTINELvariables (for example, the current index variable)of the foreach()iterator.

下面的示例示出了每个()和复位()不影响SENTINEL变量 (for example, the current index variable)的的foreach()迭代器。

$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/>";

Output:

输出:

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

回答by dkasipovic

NOTE FOR PHP 7

PHP 7 的注意事项

To update on this answer as it has gained some popularity: This answer no longer applies as of PHP 7. As explained in the "Backward incompatible changes", in PHP 7 foreach works on copy of the array, so any changes on the array itself are not reflected on foreach loop. More details at the link.

更新此答案,因为它已经获得了一些人气:此答案不再适用于 PHP 7。如“向后不兼容的更改”中所述,在 PHP 7 中 foreach 适用于数组的副本,因此对数组本身的任何更改不会反映在 foreach 循环中。链接中的更多详细信息。

Explanation (quote from php.net):

说明(引自php.net):

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

第一种形式循环遍历由 array_expression 给出的数组。在每次迭代中,当前元素的值被分配给 $value 并且内部数组指针前进一个(因此在下一次迭代中,您将查看下一个元素)。

So, in your first example you only have one element in the array, and when the pointer is moved the next element does not exist, so after you add new element foreach ends because it already "decided" that it it as the last element.

因此,在您的第一个示例中,数组中只有一个元素,当指针移动时,下一个元素不存在,因此在添加新元素后 foreach 结束,因为它已经“决定”将其作为最后一个元素。

In your second example, you start with two elements, and foreach loop is not at the last element so it evaluates the array on the next iteration and thus realises that there is new element in the array.

在您的第二个示例中,您从两个元素开始,并且 foreach 循环不在最后一个元素处,因此它会在下一次迭代中评估数组,从而意识到数组中有新元素。

I believe that this is all consequence of On each iterationpart of the explanation in the documentation, which probably means that foreachdoes all logic before it calls the code in {}.

我相信这是文档中解释的每次迭代部分的所有结果,这可能意味着foreach在调用{}.

Test case

测试用例

If you run this:

如果你运行这个:

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

You will get this output:

你会得到这个输出:

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

Which means that it accepted the modification and went through it because it was modified "in time". But if you do this:

这意味着它接受了修改并通过了它,因为它是“及时”修改的。但如果你这样做:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

You will get:

你会得到:

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

Which means that array was modified, but since we modified it when the foreachalready was at the last element of the array, it "decided" not to loop anymore, and even though we added new element, we added it "too late" and it was not looped through.

这意味着数组被修改了,但是由于我们在foreach已经在数组的最后一个元素时修改了它,它“决定”不再循环,即使我们添加了新元素,我们也“太晚了”添加它没有循环通过。

Detailed explanation can be read at How does PHP 'foreach' actually work?which explains the internals behind this behaviour.

详细解释可以在PHP 'foreach' 如何实际工作?这解释了这种行为背后的内部结构。

回答by user3535130

As per the documentation provided by PHP manual.

根据 PHP 手册提供的文档。

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

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

So as per your first example:

所以根据你的第一个例子:

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

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

$array只有单个元素,因此根据 foreach 执行,1 分配给$v并且它没有任何其他元素来移动指针

But in your second example:

但在你的第二个例子中:

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

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

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

回答by Hrvoje Antunovi?

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. For example:

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

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

This outputs:

这输出:

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

This outputs:

这输出:

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:

与原始版本的任何更改都不能作为通知,实际上与原始版本没有任何更改,即使您明确为 $item 分配了一个值。这是因为您正在对 $item 进行操作,因为它出现在正在处理的 $set 副本中。您可以通过引用获取 $item 来覆盖它,如下所示:

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

This outputs:

这输出:

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:

所以很明显可以观察到,当对 $item 进行按引用操作时,对 $item 所做的更改是对原始 $set 的成员进行的。通过引用使用 $item 还可以防止 PHP 创建数组副本。为了测试这一点,首先我们将展示一个演示副本的快速脚本:

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

This outputs:

这输出:

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.

如示例所示,PHP 复制了 $set 并使用它进行循环,但是当 $set 在循环内使用时,PHP 将变量添加到原始数组中,而不是复制的数组中。基本上,PHP 仅使用复制的数组来执行循环和分配 $item。正因为如此,上面的循环只执行了 3 次,每次都会在原 $set 的末尾追加一个值,让原 $set 有 6 个元素,但从未进入无限循环。

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

但是,如果我们通过引用使用 $item 会怎样,就像我之前提到的那样?添加到上述测试的单个字符:

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

结果无限循环。请注意,这实际上是一个无限循环,您必须自己终止脚本或等待操作系统内存不足。我在脚本中添加了以下行,因此 PHP 会很快耗尽内存,如果您要运行这些无限循环测试,我建议您也这样做:

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.

因此,在前面的无限循环示例中,我们看到了编写 PHP 以创建要循环的数组副本的原因。当创建副本并仅由循环构造本身的结构使用时,数组在整个循环执行过程中保持静态,因此您永远不会遇到问题。

回答by Pranav Rana

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

PHP foreach 循环可以与Indexed arrays,Associative arrays和一起使用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 copyof the array rather than the original one. This is demonstrated in the below example:

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

<?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 valueas well. This is demonstrated below:

除此之外,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 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 indexesto be used as references.

注意:它不允许original array indexes用作references.

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

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