提问



让我通过说我知道foreach是什么,做什么以及如何使用它来作为前缀。这个问题涉及它如何在引擎盖下工作,我不想要任何答案这是你用foreach循环数组的方式。





很长一段时间,我认为foreach与阵列本身一起工作。然后我发现许多引用它与数组的副本一起工作的事实,我已经假设这是故事的结尾。但是我最近讨论了这个问题,经过一些实验后发现这实际上并非100%正确。


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


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


测试案例1:[139]


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:[140]


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


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


如果我们查看手册,我们会发现以下声明:[141]



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



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


测试案例3:[142]


// 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将永远循环。[143]


PHP手册还指出:



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



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


测试案例4:[144]


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

/* Output: 1 2 3 4 5 */


测试案例5:[145]


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

/* Output: 1 2 3 4 5 */


......没有任何出乎意料的事实,它实际上似乎支持源头理论。





问题


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


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



  • 这是正确的还是整个故事?

  • 如果没有,它到底在做什么?

  • foreach期间使用调整数组指针的函数(each()reset()等)是否会影响循环的结果?


最佳参考


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



  • 阵列

  • 普通对象

  • Traversable个对象



在下文中,我将尝试精确解释迭代在不同情况下的工作原理。到目前为止,最简单的情况是Traversable个对象,因为这些foreach基本上只是代码沿这些方向的语法糖:[146]


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)

  • 对象具有by-handle传递语义,但必须实际意味着它们的行为类似于引用。因此,在迭代期间总是可以更改对象。



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


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


作为最后的初步,应该注意PHP使用引用计数和写时复制来管理内存。这意味着如果您复制一个值,实际上只是重用旧值并增加其引用计数(refcount)。只有在执行某种修改后,才会执行真正的副本(称为复制)。看到你被骗了就这个话题进行更广泛的介绍。[147]


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会将指向当前元素及其散列的指针备份到per-foreach HashPointer中。循环体运行后,如果IAP仍然存在,IAP将被设置回该元素。然而,如果该元素已被删除,我们将只使用IAP当前所处的位置。这种方案大多数有点有用,但是你可以从中获得很多奇怪的行为,其中一些我会演示如下。


数组重复



IAP是数组的可见特征(通过current函数族公开),因为IAP计数的这种变化是在写时复制语义下的修改。不幸的是,这意味着foreach在很多情况下被迫复制它迭代的数组。确切的条件是:



  1. 数组不是引用(is_ref=0)。如果它是一个引用,那么对它的更改假定要传播,所以它不应该重复。

  2. 数组的refcount> 1。如果refcount为1,那么数组不会被共享,我们可以直接修改它。



如果数组没有重复(is_ref=0,refcount=1),那么只有它的引用计数会递增(*)。此外,如果使用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)不递增引用计数,从而允许迭代数组在循环中任意修改,或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。这就是为什么在迭代期间显示修改的代码示例将始终取消设置 next 元素而不是当前元素的原因。


示例:您的测试用例



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


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



  • 在测试用例1和$array中,refcount=1开始,因此foreach不会复制它:只有refcount会递增。当循环体随后修改数组(在该点具有refcount=2)时,将在该点处进行复制。 Foreach将继续致力于未经修改的$array副本。

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

  • 在测试用例4和5中,eachreset都是参考函数。 $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),即使它没有修改数组。它必须是为了与next等所有其他函数一起使用,这些函数都是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在同一个数组上工作。由于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的引用计数为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,它已被内部循环标记为已完成!


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将从一开始就迭代另一个实体。


PHP 7



Hashtable迭代器



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


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


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


数组重复



PHP 5和PHP 7之间的另一个重要变化涉及阵列复制。现在不再使用IAP,在所有情况下,按值数组迭代只会执行引用计数增量(而不是重复数组)。如果在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保留相同的输出:按值数组迭代始终保持对原始元素的处理。 (在这种情况下,甚至引用和重复行为在PHP 5和PHP 7之间完全相同)。

  • 测试用例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恢复机制直接跳转到新元素,因为它看起来像是与remove元素相同(由于碰撞h灰和指针)。由于我们不再依赖元素哈希来解决任何问题,因此这不再是一个问题。

其它参考1


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


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三元运算符:快还是不快?[149]

其它参考2


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


a)foreach适用于原始数组的预期副本
    这意味着foreach()将具有SHARED数据存储,直到或除非prospected copy
    没有创建foreach Notes/User comments。[150]


b)触发预期副本的原因是什么?
    预期副本是根据copy-on-write的政策创建的,即每当
    传递给foreach()的数组被更改,创建了原始数组的克隆。


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


Stack 溢出问题如何确保在PHP的foreach循环中重置值?解决了问题的案例(3,4,5)。


以下示例显示each()和reset()不会影响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 => 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 7注意


要更新这个答案,因为它已经获得了一些普及:这个答案不再适用于PHP 7.正如在向后不兼容的更改中所解释的那样,在PHP 7中,foreach适用于数组的复制,因此对数组本身的任何更改没有反映在foreach循环上。链接的更多细节。[155]


解释(引自php.net): [156]



  第一种形式循环遍历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'如何实际工作中阅读?这解释了这种行为背后的内部因素。

其它参考4


根据PHP手册提供的文档。



  在每次迭代中,当前元素的值分配给$ v和内部

  数组指针先进一步(所以在下一次迭代中,你将看到下一个元素)。



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


$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计算零索引并将指针移动一个。对于循环的第一次迭代,将$array['baz']=3;添加为通过引用传递。

其它参考5


很好的问题,因为许多开发人员,甚至是经验丰富的开发人员都对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的副本中出现$ item你可以通过引用抓取$ 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来创建要循环的数组副本的原因。当副本被创建并仅由循环结构本身的结构使用时,该数组在循环执行期间保持静态,因此您永远不会遇到问题。

其它参考6


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


除此之外,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


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


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