让我先说一下,我知道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()等)是否会影响循环的结果?


当前回答

使用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

其他回答

在示例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三元运算符:快还是不快?

这个问题很好,因为许多开发人员,甚至是经验丰富的开发人员,都对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以创建要循环的数组副本的原因。当创建副本并仅由循环构造本身的结构使用时,阵列在循环的整个执行过程中保持静态,因此您不会遇到问题。

使用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

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

阵列普通对象可遍历对象

在下文中,我将尝试准确解释迭代在不同情况下的工作原理。到目前为止,最简单的情况是可遍历对象,因为对于这些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中,“数组”是真正有序的字典,它们将按照这个顺序进行遍历(只要不使用排序之类的东西,这与插入顺序相匹配)。这与按键的自然顺序(其他语言的列表通常是如何工作的)或根本没有定义的顺序(其他其他语言的字典通常是如何运行的)进行迭代是相反的。

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

到目前为止,一切都很好。反复阅读字典不会太难,对吧?当您意识到数组/对象在迭代过程中可能发生变化时,问题就开始了。有多种方式可以实现这一点:

如果使用foreach($arr as&$v)通过引用进行迭代,则$arr将变为引用,您可以在迭代期间更改它。在PHP5中,即使按值迭代,也同样适用,但数组事先是一个引用:$ref=&$arr;foreach($ref为$v)对象具有通过句柄传递的语义,这对于大多数实际目的来说意味着它们的行为类似于引用。因此,在迭代过程中始终可以更改对象。

允许在迭代过程中进行修改的问题是,您当前所在的元素被删除。假设你使用一个指针来跟踪你当前所在的数组元素。如果现在释放了这个元素,你就会得到一个悬空的指针(通常会导致segfault)。

解决这个问题有不同的方法。PHP5和PHP7在这方面有很大不同,我将在下面描述这两种行为。总结是,PHP5的方法相当愚蠢,并导致了各种奇怪的边缘案例问题,而PHP7的更复杂的方法导致了更可预测和一致的行为。

作为最后一个预备,应该注意,PHP使用引用计数和写时复制来管理内存。这意味着,如果“复制”一个值,实际上只需重用旧值并增加其引用计数(refcount)。只有在您执行某种修改后,才会进行真正的复制(称为“复制”)。请参阅“你被欺骗了”,了解有关此主题的更详细介绍。

第5页

内部数组指针和HashPointer

PHP5中的数组有一个专用的“内部数组指针”(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执行以下操作:在执行循环体之前,foreach会将一个指向当前元素的指针及其哈希备份到每个foreach HashPointer中。循环体运行后,如果IAP仍然存在,IAP将被设置回该元素。然而,如果元素已经被删除,我们将只使用IAP当前所在的位置。这个方案基本上是可行的,但你可以从中得到很多奇怪的行为,我将在下面演示其中一些行为。

阵列复制

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

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

如果数组不重复(is_ref=0,refcount=1),则只有其refcount会递增(*)。此外,如果使用foreach by reference,则(可能重复的)数组将被转换为引用。

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

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

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

在这里,$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(PHP7解决方案)。

职位晋升顺序

为了正确理解下面的代码示例,您必须了解最后一个实现细节。在伪代码中,循环某些数据结构的“正常”方式类似于:

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上。这就是为什么在迭代期间显示修改的代码样本总是取消设置下一个元素,而不是当前元素的原因。

示例:您的测试用例

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

此时,测试用例的行为很容易解释:

在测试用例1和2中,$array以refcount=1开始,因此foreach不会复制它:只有refcount递增。当循环体随后修改数组(此时refcount=2)时,复制将在该点发生。Foreach将继续处理$array的未修改副本。在测试用例3中,数组再次不重复,因此foreach将修改$array变量的IAP。在迭代结束时,IAP为NULL(意味着迭代已经完成),每个IAP都返回false。在测试用例4和5中,每个和重置都是参考函数。$数组在传递给它们时具有refcount=2,因此必须复制它。因此,foreach将再次在单独的数组上工作。

示例:foreach中电流的影响

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

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

这里您应该知道current()是一个by-ref函数(实际上是:preferref),尽管它不修改数组。它必须能够很好地处理所有其他函数,比如next,这些函数都是通过引用实现的。通过引用传递意味着数组必须分开,因此$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在同一个数组上工作。尽管如此,由于foreach前进指针的方式,您仍然可以通过一个行为来完成。

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

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

这里重要的一点是,foreach在通过引用迭代时会使$array成为is_ref=1,因此基本上与上面的情况相同。

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

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

在这里,当循环开始时,$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已删除。可能出乎意料的是,外循环在第一个元素之后停止。为什么?

这背后的原因是上面描述的嵌套循环黑客:在循环体运行之前,将当前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将从一开始就开始迭代其他实体。

第7页

哈希表迭代器

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

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

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

阵列复制

PHP5和PHP7之间的另一个重要变化与数组复制有关。既然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 */

以前,引用数组的按值迭代是特殊情况。在这种情况下,不会发生重复,因此迭代期间对数组的所有修改都将由循环反映。在PHP7中,这种特殊情况已经消失:数组的逐值迭代将始终在原始元素上工作,而忽略循环期间的任何修改。

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

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

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

示例

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

测试用例1和2保持相同的输出:按值数组迭代始终在原始元素上工作。(在本例中,PHP5和PHP7之间的重复计数和重复行为完全相同)。测试用例3更改:Foreach不再使用IAP,因此each()不受循环影响。前后输出相同。测试用例4和5保持不变:each()和reset()将在更改IAP之前复制数组,而foreach仍然使用原始数组。(即使阵列是共享的,IAP的更改也不重要。)

第二组示例与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还原机制跳到新元素,因为它“看起来”与移除的元素相同(由于哈希和指针冲突)。由于我们不再依赖元素哈希来做任何事情,这不再是一个问题。

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”实际上是如何工作的?这解释了这种行为背后的内在原因。