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

How does PHP 'foreach' actually work?

提问人:DaveRandom 提问时间:4/8/2012 最后编辑:sergiolDaveRandom 更新时间:11/15/2023 访问量:456020

问:

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


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

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

$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 期间使用调整数组指针 (, et al.) 的函数可能会影响循环的结果吗?each()reset()foreach
迭代 循环 php-internals

评论

8赞 Michael Berkowski 4/8/2012
@DaveRandom 可能应该有一个 php-internals 标签,但我会让你决定替换其他 5 个标签中的哪一个(如果有的话)。
6赞 zb' 4/8/2012
看起来像 COW,没有删除句柄
176赞 knittl 4/8/2012
起初我想»天哪,另一个新手问题。阅读文档...嗯,明显未定义的行为«。然后我读了完整的问题,我必须说:我喜欢它。你已经付出了相当多的努力,并编写了所有的测试用例。PS. 测试用例 4 和 5 是一样的吗?
24赞 Niko 4/8/2012
只是想想为什么数组指针被触摸是有意义的:PHP 需要重置并移动原始数组的内部数组指针以及副本,因为用户可能会要求引用当前值 () - PHP 需要知道原始数组中的当前位置,即使它实际上是在迭代副本。foreach ($array as &$value)
6赞 Oliver Charlesworth 2/28/2013
@Sean:恕我直言,PHP文档在描述核心语言功能的细微差别方面确实很糟糕。但那也许是因为语言中融入了如此多的特殊情况......

答:

127赞 linepogl 4/8/2012 #1

在示例 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.

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

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

评论

0赞 zb' 4/8/2012
似乎是你的对的,我做了一些例子来证明这一点:codepad.org/OCjtvu8r 你的例子有一个区别 - 如果你改变值,它不会复制,只有在改变键时才会复制。
1赞 DaveRandom 4/8/2012
这确实解释了上面显示的所有行为,并且可以通过在第一个测试用例的末尾调用来很好地说明这一点,我们看到原始数组的数组指针指向第二个元素,因为数组在第一次迭代期间被修改了。这似乎也表明,在执行循环的代码块之前,会移动数组指针,这是我没想到的 - 我本来以为它会在最后这样做。非常感谢,这为我很好地解决了问题。each()foreach
57赞 sakhunzai 4/8/2012 #2

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

a) 在原始数组的预期副本上工作。 这意味着将具有共享数据存储,直到或除非 不是为每个注释/用户注释创建的。foreachforeach()prospected copy

b) 什么触发了预期的副本? 根据 的策略创建目标副本,即每当 传递到 的数组被更改,原始数组的克隆被创建。copy-on-writeforeach()

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

Stack Overflow 问题 如何确保在 PHP 的“foreach”循环中重置值? 解决了问题的情况 (3,4,5)。

以下示例显示 each() 和 reset() 不会影响迭代器的变量。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赞 linepogl 4/8/2012
你的答案不太正确。 对数组的潜在副本进行操作,但除非需要,否则它不会创建实际副本。foreach
0赞 sakhunzai 4/9/2012
您想演示如何以及何时通过代码创建该潜在副本吗?我的代码演示了 100% 的时间复制数组。我很想知道。感谢您的评论foreach
0赞 linepogl 4/10/2012
复制数组的成本很高。尝试使用 或 计算迭代包含 100000 个元素的数组所需的时间。您不会看到它们两者之间存在任何显着差异,因为不会发生实际的副本。forforeach
0赞 sakhunzai 4/10/2012
然后我会假设有保留的 until 或 unless ,但是(从我的代码片段中)很明显,总会有两组,一组是 for 的,另一组是 for 的。谢谢,这是有道理的SHARED data storagecopy-on-writeSENTINEL variablesoriginal arrayforeach
1赞 sakhunzai 4/16/2014
是的,这是“潜在”副本,即“潜在”副本。它没有像你建议的那样受到保护
1827赞 NikiC 2/13/2013 #3

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

在下文中,我将尝试精确地解释迭代在不同情况下的工作原理。到目前为止,最简单的情况是对象,因为这些本质上只是代码的语法糖:Traversableforeach

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中,“数组”实际上是有序的字典,它们将按照这个顺序遍历(只要你不使用类似的东西,它就与插入顺序相匹配)。这与按键的自然顺序(其他语言中的列表通常如何工作)或根本没有定义的顺序(其他语言的字典通常如何工作)相反。sort

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

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

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

在迭代过程中允许修改的问题在于,您当前所在的元素被删除了。假设您使用指针来跟踪当前所在的数组元素。如果此元素现在已释放,则会留下一个悬空指针(通常会导致段错误)。

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

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

菲律宾比索 5

内部数组指针和 HashPointer

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

虽然确实使用了 IAP,但还有一个额外的复杂性:只有一个 IAP,但一个数组可以是多个循环的一部分:foreachforeach

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

要仅使用一个内部数组指针支持两个同时循环,请执行以下恶作剧: 在执行循环体之前,将指向当前元素的指针及其哈希值备份到 per-foreach 中。循环体运行后,如果 IAP 仍然存在,则 IAP 将设置回此元素。但是,如果该元素已被删除,我们将只使用 IAP 当前所在的任何位置。这个方案基本上是有效的,但你可以从中得到很多奇怪的行为,其中一些我将在下面演示。foreachforeachHashPointer

阵列复制

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

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

如果数组不重复 (is_ref=0, refcount=1),则只会增加其数组 (*)。此外,如果使用按引用,则(可能重复的)数组将转换为引用。refcountforeach

请考虑以下代码作为发生重复的示例:

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

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

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

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

职位晋升顺序

您必须注意最后一个实现细节,才能正确理解下面的代码示例。循环遍历某些数据结构的“正常”方式在伪代码中如下所示:

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

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

示例:测试用例

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

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

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

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

  • 在测试用例 4 和 5 中,两者都是按引用的函数。当它传递给他们时,它必须被复制。因此,将再次处理单独的数组。eachreset$arrayrefcount=2foreach

示例:in foreach 的影响current

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

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

在这里你应该知道这是一个 by-ref 函数(实际上是:prefer-ref),即使它不修改数组。它必须与所有其他函数很好地配合,例如它们都是 by-ref。By-reference 传递意味着数组必须分开,因此 和 将不同。你得到 instead 的原因在上面也提到过:在运行用户代码之前,而不是之后,推进数组指针。因此,即使代码位于第一个元素上,也已经将指针推进到第二个元素。current()next$arrayforeach-array21foreachforeach

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

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

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

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

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

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

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

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

当循环开始时,这里的 refcount 是 2,所以这一次我们实际上必须预先进行复制。因此,foreach 使用的数组从一开始就是完全独立的。这就是为什么您可以在循环之前的任何位置获得 IAP 的位置(在本例中,它位于第一个位置)。$array$array

示例:迭代期间的修改

尝试解释迭代过程中的修改是我们所有 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 位置和哈希值被备份到 .在循环体之后,它将被恢复,但前提是元素仍然存在,否则将改用当前的 IAP 位置(无论它是什么)。在上面的示例中,情况正是如此:外部循环的当前元素已被删除,因此它将使用 IAP,该 IAP 已被内部循环标记为已完成!HashPointer

备份+还原机制的另一个结果是,通过等方式对 IAP 的更改通常不会影响 。例如,以下代码执行时,就好像 根本不存在一样:HashPointerreset()foreachreset()

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

原因是,在临时修改 IAP 的同时,它将恢复到循环体之后的当前 foreach 元素。若要强制对循环产生影响,必须额外删除当前元素,以便备份/还原机制失败:reset()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

但是,这些例子仍然是理智的。如果您记得还原使用指向元素及其哈希值的指针来确定它是否仍然存在,那么真正的乐趣就开始了。但是:哈希有冲突,指针可以重用!这意味着,通过仔细选择数组键,我们可以相信已被删除的元素仍然存在,因此它将直接跳转到它。举个例子:HashPointerforeach

$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

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

在循环中替换迭代的实体

我想提的最后一个奇怪的例子是,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 循环以及与 etc 的交互。reset()

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

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

阵列复制

PHP 5 和 PHP 7 之间的另一个重要变化与数组复制有关。现在不再使用 IAP,在所有情况下,按值数组迭代将仅执行增量(而不是复制数组)。如果在循环期间修改了数组,则此时将发生重复(根据写入时复制),并将继续在旧数组上工作。refcountforeachforeach

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

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

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

例子

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

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

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

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

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

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

$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赞 NikiC 2/13/2013
@Baba确实如此。将其传递给函数与在循环;)之前执行的操作相同$foo = $array
0赞 Arnaud Le Blanc 2/28/2013
foreach 似乎在迭代后将数组的位置保存在结构中,并在迭代之前恢复它。该结构体带有 和 的桶。哈希值用于验证存储桶是否仍在表中,因此即使在修改数组后也可以安全使用 a。这可能就是为什么我们可以看到修改后的数组位置(在 foreach 之后返回 null),以及为什么尝试修改它不会影响 foreach。HashPointerHashPositionhHashPositionHashPointereach()
36赞 shu zOMG chen 2/28/2013
对于那些不知道 zval 是什么的人,请参阅 Sara Goleman 的 blog.golemon.com/2007/01/youre-being-lied-to.html
1赞 unbeli 3/1/2013
小更正:您所说的 Bucket 不是哈希表中通常所说的 Bucket。通常,Bucket 是一组具有相同哈希百分比大小的条目。您似乎将它用于通常称为条目的内容。链表不在存储桶上,而是在条目上。
13赞 NikiC 3/1/2013
@unbeli我使用的是PHP内部使用的术语。s 是哈希冲突的双向链表的一部分,也是订单;)的双向链表的一部分Bucket
41赞 dkasipovic 4/15/2014 #4

PHP 7 注释

更新这个答案,因为它已经获得了一些流行:这个答案从 PHP 7 开始不再适用。正如 “向后不兼容的更改” 中所解释的,在 PHP 7 中,foreach 在数组的副本上工作,因此对数组本身的任何更改都不会反映在 foreach 循环中。更多详情请见链接。

解释(引自 php.net):

第一种形式遍历 array_expression 给出的数组。在每个 迭代时,将当前元素的值赋值给 $value 和 内部数组指针前进 1(以此类推 迭代,您将查看下一个元素)。

因此,在您的第一个示例中,数组中只有一个元素,当指针移动时,下一个元素不存在,因此在添加新元素后,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赞 dkasipovic 4/15/2014
那么,你读过其余的答案了吗?foreach 甚至在运行其中的代码之前就决定是否再循环一次是完全有道理的。
2赞 dkasipovic 4/15/2014
不,数组被修改了,但“为时已晚”,因为 foreach 已经“认为”它位于最后一个元素(它在迭代开始时)并且不会再循环。在第二个示例中,它不是迭代开始时的最后一个元素,而是在下一次迭代开始时再次计算。我正在尝试准备一个测试用例。
1赞 bwoebi 4/15/2014
@AlmaDo 查看 lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 当它迭代时,它始终设置为下一个指针。因此,当它到达最后一次迭代时,它将被标记为已完成(通过 NULL 指针)。然后,当您在上次迭代中添加键时,foreach 不会注意到它。
1赞 Alma Do 4/15/2014
@DKasipovic没有。那里没有完整和明确的解释(至少现在 - 可能是我错了)
4赞 bwoebi 4/15/2014
其实@AlmaDo似乎在理解自己的逻辑上有一个缺陷......你的答案很好。
18赞 user3535130 4/15/2014 #5

根据 PHP 手册提供的文档。

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

因此,根据您的第一个示例:

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

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

但在第二个示例中:

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

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

15赞 Hrvoje Antunović 4/21/2017 #6

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

10赞 Pranav Rana 11/13/2017 #7

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 indexesreferences

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

评论

1赞 Christian 12/27/2017
Object public variables是错误的或充其量是误导性的。你不能在没有正确接口(例如,Traversible)的情况下使用数组中的对象,当你这样做时,你实际上是在使用一个简单的数组,而不是一个对象。foreach((array)$obj ...
-2赞 svrl 11/15/2023 #8

您的问题可以从多个角度来理解。PHP Foreach 是一个循环,它允许您遍历数组类型。正如预期的那样,根据 Marketsplash 执行:

  1. 数组初始化;
  2. 启动 foreach 循环;
  3. 处理每个项目。

正如您提到的,可以在项目上应用或不应用符号的 foreach 来实际链接到原始数组以直接更改原始数组 - 考虑下面来自 Marketsplash 的示例:&

$scores = array(50, 60, 70, 80);

foreach ($scores as &$score) {
    $score += 10; // Adds 10 to each score
}

// $scores is now array(60, 70, 80, 90)

评论

0赞 DarkBee 11/15/2023
这个答案除了已经存在的答案之外没有增加任何新的东西......
0赞 Nico Haase 11/15/2023
特别是,这个答案没有解释 foreach 在内部是如何工作的。“数组初始化”到底是什么意思?