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


2018

首先,我要说一下我知道它是什么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可以处理数组的副本,但是在循环之后将源数组的数组指针设置为数组的末尾。

  • 这是正确的故事吗?
  • 如果没有,它到底在做什么?
  • 是否存在在a期间使用调整数组指针(each()reset()等)的函数foreach会影响循环结果的情况?

5
@DaveRandom有一个php-internals标记可能应该使用,但我将由您决定是否要替换其他5个标记中的任何一个。
Michael Berkowski 2012年

5
看起来像COW,没有删除句柄
zb'2012年

149
起初,我想到了»天哪,另一个新手问题。阅读文档…hm,显然是未定义的行为«。然后,我阅读了完整的问题,我必须说:我喜欢它。您已经付出了很多努力并编写了所有测试用例。ps。测试用例4和5相同吗?
knittl 2012年

21
只是想一想为什么触摸数组指针确实有意义:PHP需要重置并移动原始数组的内部数组指针以及副本,因为用户可能会要求引用当前值(foreach ($array as &$value))- PHP需要知道原始数组中的当前位置,即使它实际上是在副本上进行迭代。
Niko 2012年

4
@Sean:恕我直言,PHP文档确实很难描述核心语言功能的细微差别。但这也许是因为在语言中包含了许多特殊的特殊情况……
Oliver Charlesworth

Answers:


1660

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

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

对于内部类,通过使用实质上只是Iterator在C级别上镜像接口的内部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使用引用计数和写时复制来管理内存。这意味着,如果您“复制”一个值,则实际上只是重用旧值并增加其引用计数(refcount)。仅当您执行某种修改后,才会完成真实副本(称为“复制”)。请参阅“ 您被骗了”,以获取有关此主题的更广泛的介绍。

PHP 5

内部数组指针和HashPointer

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

尽管foreach确实使用了IAP,但还有一个复杂之处:只有一个IAP,但是一个数组可以成为多个foreach循环的一部分:

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

若要仅使用一个内部数组指针支持两个同时循环,请foreach执行以下操作:在循环体执行之前,foreach将指向当前元素的指针及其哈希值备份到per-foreach中HashPointer。循环主体运行后,如果IAP仍然存在,则将其设置回该元素。但是,如果该元素已删除,我们将仅使用IAP当前所在的位置。这个方案主要是某种类型的作品,但是您可以摆脱很多奇怪的行为,下面将对其中的一些进行演示。

阵列复制

IAP是数组的可见功能(通过current函数族公开),因此,IAP计数的更改是写时复制语义下的修改。不幸的是,这意味着foreach在很多情况下被迫复制要迭代的数组。精确的条件是:

  1. 该数组不是引用(is_ref = 0)。如果是参考,则应该传播对其的更改,因此不应重复。
  2. 数组的引用计数> 1。如果refcount为1,则不共享该数组,我们可以自由地直接对其进行修改。

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

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

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

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

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

(*)在refcount此处递增听起来很无害,但违反了写时复制(COW)语义:这意味着我们将修改refcount = 2数组的IAP,而COW指示只能对refcount = 1个值。违反行为会导致用户可见的行为更改(而COW通常是透明的),因为可以观察到迭代阵列上的IAP更改-但仅在阵列上进行首次非IAP修改之前。取而代之的是,三个“有效”选项是:a)始终重复,b)不递增refcount,因此允许在循环中随意修改迭代数组,或者c)根本不使用IAP(PHP) 7解)。

职位提升订单

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

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

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

示例:您的测试用例

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

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

  • 在测试用例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将再次在单独的阵列上工作。

示例:currentin foreach的效果

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

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

在这里,您应该知道这current()是一个by-ref函数(实际上是:referr-ref),即使它没有修改数组。它必须是为了与所有其他功能(如nextby-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-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已被删除。出乎意料的是,外循环在第一个元素之后停止。这是为什么?

其背后的原因是上述的嵌套循环hack:在循环主体运行之前,将当前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就会从头开始迭代另一个实体。

PHP 7

哈希表迭代器

如果您还记得,数组迭代的主要问题是如何处理元素在迭代过程中的移除。为此,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保留相同的输出:按值数组迭代始终对原始元素起作用。(在这种情况下,refcountingPHP 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还原机制直接跳转到新元素,因为它“看起来”与删除的元素相同(由于哈希和指针冲突)。由于我们不再依赖元素哈希进行任何操作,因此这不再是问题。


4
@Baba确实如此。将其传递给函数$foo = $array与循环之前相同;)
NikiC

32
对于那些不知道zval是什么的人,请参阅Sara Goleman的blog.golemon.com/2007/01/youre-being-lied-to.html
shu zOMG chen

1
较小的更正:您所说的Bucket不是哈希表中通常所说的Bucket。通常,Bucket是具有相同hash%size的一组条目。您似乎将其用于通常称为条目的项目。链接列表不在存储桶中,而是在条目上。
unbeli

12
@unbeli我正在使用PHP内部使用的术语。该Buckets为哈希冲突的双向链表,也为顺序双向链表的一部分的一部分;)
NikiC

4
太棒了。我想你的意思iterate($outerArr);不是在iterate($arr);某个地方。
niahoo

116

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


似乎是您的权利,我举了一个例子来说明这一点: codepad.org/OCjtvu8r与您的例子有一个区别-如果更改值,只有更改键,它才会复制。
zb'2012年

这确实可以解释上面显示的所有行为,并且可以通过each()在第一个测试用例的末尾进行调用来很好地说明,在该示例中,我们看到原始数组的数组指针指向第二个元素,因为该数组是在第一次迭代。这似乎也证明了foreach在执行循环的代码块之前将数组指针移到了我没想到的-我本来以为它将在最后执行此操作。非常感谢,这对我来说很好。
DaveRandom 2012年

49

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

a)处理原始阵列foreach预期副本。这意味着foreach()将共享数据存储,直到或除非prospected copy未为每个Notes /用户注释创建a 。

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

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

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

下面的示例示出了每个()和复位()不影响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/>";

输出:

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

2
您的答案不太正确。foreach对阵列的潜在副本进行操作,但除非需要,否则不会生成实际副本。
linepogl 2012年

您想演示通过代码创建潜在副本的方式和时间吗?我的代码演示foreach了100%的时间正在复制数组。我很想知道。谢谢您的留言
sakhunzai 2012年

复制阵列会花费很多。尝试计算使用for或迭代具有100000个元素的数组所需的时间foreach。您将不会看到两者之间的任何显着差异,因为不会进行实际复制。
linepogl 2012年

然后我认为 SHARED data storage保留了直到或除非copy-on-write,但(从我的代码段中)显而易见的是,总是会有两套,一套SENTINEL variables用于original array,另一套用于foreach。谢谢,这很有道理
sakhunzai 2012年

1
是的,这是“预期”副本,即“潜在”副本。它未按照您的建议进行保护
sakhunzai 2014年

33

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'实际如何工作中阅读详细说明这解释了此行为的内部原因。


7
您是否读完了其余的答案?完全有意义的是,foreach决定是否它甚至运行其中的代码之前再循环一次。
dkasipovic 2014年

2
不,数组被修改了,但是“太迟了”,因为foreach已经“认为”它在最后一个元素(它在迭代的开始)并且不再循环。在第二个示例中,它不在迭代开始时的最后一个元素处,而是在下一个迭代开始时再次求值。我正在尝试准备一个测试用例。
dkasipovic 2014年

1
@AlmaDo请查看lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509迭代时,它始终设置为下一个指针。因此,当到达最后一次迭代时,它将被标记为完成(通过NULL指针)。当您在最后一次迭代中添加密钥时,foreach不会注意到它。
bwoebi 2014年

1
@DKasipovic没有 那里没有完整明确的解释(至少目前为止-可能我错了)
Alma Do

4
实际上,@ AlmaDo在理解自己的逻辑上似乎有缺陷……您的回答很好。
bwoebi 2014年

15

根据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;为通过引用传递。


13

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


7

PHP foreach循环可以与使用Indexed arraysAssociative arrays以及Object 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


1
Object public variables是错误的,或充其量只能是误导。没有正确的接口(例如,Traversible),就无法使用数组中的对象,而foreach((array)$obj ...实际上,您正在使用简单数组而不是对象。
基督徒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.