关于循环:php”foreach”实际上如何工作?

How does PHP 'foreach' actually work?

我先说一下,我知道foreach是什么,它的用途和使用方法。这个问题涉及到它在引擎盖下的工作方式,我不想回答"这是如何用foreach循环数组"这一行。

很长一段时间以来,我假设foreach与数组本身一起工作。然后我发现了许多关于它与数组的副本一起工作的引用,并且从那时起我就假定这是故事的结尾。但我最近就此事进行了讨论,经过一点试验,发现事实并非100%正确。

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

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

测试用例1:

1
2
3
4
5
6
7
8
9
foreach ($array as $item) {
  echo"$item
"
;
  $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:

1
2
3
4
5
6
7
8
9
10
foreach ($array as $key => $item) {
  $array[$key + 1] = $item + 2;
  echo"$item
"
;
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 3 4 5 6 7 */

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

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

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

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

测试用例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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
"
;
}

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手册》还规定:

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

好吧,让我们看看"意外行为"是什么(从技术上讲,任何行为都是意外的,因为我不再知道该期待什么)。

测试用例4:

1
2
3
4
5
6
7
foreach ($array as $key => $item) {
  echo"$item
"
;
  each($array);
}

/* Output: 1 2 3 4 5 */

测试用例5:

1
2
3
4
5
6
7
foreach ($array as $key => $item) {
  echo"$item
"
;
  reset($array);
}

/* Output: 1 2 3 4 5 */

…没有什么出乎意料的,事实上,它似乎支持"复制源"理论。

问题

这是怎么回事?我的c-fu不够好,我无法通过查看PHP源代码来提取正确的结论,如果有人能为我将其翻译成英语,我将不胜感激。

在我看来,foreach使用数组的副本,但在循环之后将源数组的数组指针设置到数组的末尾。

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


foreach支持对三种不同的值进行迭代:好的。

  • 数组
  • 正常物体
  • Traversable对象

在下面,我将尝试精确地解释迭代如何在不同的情况下工作。到目前为止,最简单的情况是Traversable对象,对于这些foreach对象,基本上只是这些行代码的语法糖:好的。

1
2
3
4
5
6
7
8
9
10
11
12
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)这样的东西,它们就与插入顺序相匹配。这与按键的自然顺序迭代(其他语言的列表通常如何工作)或完全没有定义的顺序(其他语言的字典通常如何工作)相反。好的。

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

到目前为止,一切都很好。对字典进行迭代不会太难,对吧?当您意识到数组/对象可以在迭代期间更改时,问题就开始了。这种情况有多种发生方式:好的。

  • 如果使用foreach ($arr as &$v)通过引用进行迭代,那么$arr将变成引用,并且可以在迭代期间更改它。
  • 在php 5中,即使按值迭代,也同样适用,但数组是预先引用的:$ref =& $arr; foreach ($ref as $v)
  • 对象具有按句柄传递语义,这对于大多数实际用途来说意味着它们的行为类似于引用。所以对象总是可以在迭代期间更改。

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

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

作为最后一个预备,应该注意PHP使用引用计数和写时复制来管理内存。这意味着,如果您"复制"一个值,实际上只需重用旧值并增加其引用计数(refcount)。只有在您执行某种修改之后,才会进行真正的复制(称为"复制")。关于这个话题的更广泛的介绍,请看你被欺骗了。好的。PHP 5内部数组指针和哈希指针

php 5中的数组有一个专用的"内部数组指针"(iap),它正确地支持修改:每当删除一个元素时,都会检查iap是否指向这个元素。如果是这样,它将被提升到下一个元素。好的。

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

1
2
3
4
5
6
7
// 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将向上备份一个指向当前元素的指针及其散列到每个foreach HashPointer中。循环体运行后,如果IAP仍然存在,它将被设置回该元素。但是,如果元素已被删除,我们将仅在IAP当前所在的位置使用。这个方案基本上是可行的,但是你可以从中得到很多奇怪的行为,我将在下面演示其中的一些。好的。阵列复制

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

  • 数组不是引用(is_ref=0)。如果它是一个引用,那么对它所做的更改应该传播,因此不应该复制它。
  • 数组的refCount大于1。如果refcount为1,则数组不共享,我们可以直接修改它。
  • 如果数组不重复(is_ref=0,refcount=1),则只有其refcount才会递增(*)。此外,如果使用了foreach by reference,则(可能重复的)数组将转换为引用。好的。

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

    1
    2
    3
    4
    5
    6
    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听起来无害,但违反了copy-on-write(cow)语义:这意味着我们将修改refcount=2数组的IAP,而cow则指示只能对refcount=1值执行修改。这种违反会导致用户可见的行为更改(而COW通常是透明的),因为迭代数组上的IAP更改是可观察的——但只有在对数组进行第一次非IAP修改之前。相反,三个"有效"选项应该是a)始终重复,b)不增加refcount,从而允许在循环中任意修改迭代数组,或c)根本不使用iap(php 7解决方案)。好的。职位晋升令

    为了正确理解下面的代码示例,您必须了解最后一个实现细节。在伪代码中,通过某些数据结构进行循环的"正常"方式如下所示:好的。

    1
    2
    3
    4
    5
    reset(arr);
    while (get_current_data(arr, &data) == SUCCESS) {
        code();
        move_forward(arr);
    }

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

    1
    2
    3
    4
    5
    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为空(表示迭代已经完成),由each返回false表示。好的。

    • 在测试用例4和5中,eachreset都是通过引用函数实现的。$array在传递给它们时有一个refcount=2,因此必须复制。因此,foreach将再次在单独的阵列上工作。好的。

    示例:current对foreach的影响

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

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

    在这里,您应该知道current()是一个by-ref函数(实际上:prefer-ref),即使它不修改数组。它必须能够很好地处理所有其他的函数,如next,这些函数都是通过引用传递的。通过引用传递意味着数组必须被分离,因此$array和foreach数组将不同。上面也提到了使用2而不是1的原因:foreach在运行用户代码之前,而不是之后,提前数组指针。因此,尽管代码位于第一个元素,foreach已经将指针移到第二个元素。好的。

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

    1
    2
    3
    4
    5
    $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前进指针的方式,您仍然可以通过一种行为看到关闭。好的。

    当按引用迭代执行时,您会得到相同的行为:好的。

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

    这里重要的部分是,当通过引用迭代时,foreach将使$arrayan is_ref=1,因此基本上与上面的情况相同。好的。

    另一个小的变化,这次我们将把数组赋给另一个变量:好的。

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

    在这里,循环开始时,EDOCX1的refcount(0)是2,因此我们实际上必须提前进行复制。因此,foreach使用的$array和数组将从一开始就完全分离。这就是为什么你得到了IAP的位置,不管它在循环之前在哪里(在本例中,它在第一个位置)。好的。示例:迭代期间的修改

    在迭代过程中考虑修改是我们所有foreach问题的起源,因此可以考虑这个例子。好的。

    考虑相同数组上的这些嵌套循环(其中,by-ref迭代用于确保它确实是相同的循环):好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    foreach ($array as &$v1) {
        foreach ($array as &$v2) {
            if ($v1 == 1 && $v2 == 1) {
                unset($array[1]);
            }
            echo"($v1, $v2)
    "
    ;
        }
    }

    // Output: (1, 1) (1, 3) (1, 4) (1, 5)

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

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

    HashPointer备份+恢复机制的另一个后果是,虽然reset()等的更改通常不会影响到foreach,但对iap的更改。例如,执行以下代码时,就好像reset()根本不存在一样:好的。

    1
    2
    3
    4
    5
    6
    $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()对循环产生影响,必须另外删除当前元素,以便备份/恢复机制失败:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    $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相信已删除的元素仍然存在,因此它将直接跳到该元素上。一个例子:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $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允许您在循环期间替换迭代的实体。所以您可以开始迭代一个数组,然后在中途用另一个数组替换它。或者开始迭代数组,然后用对象替换它:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $arr = [1, 2, 3, 4, 5];
    $obj = (object) [6, 7, 8, 9, 10];

    $ref =& $arr;
    foreach ($ref as $val) {
        echo"$val
    "
    ;
        if ($val == 3) {
            $ref = $obj;
        }
    }
    /* Output: 1 2 3 6 7 8 9 10 */

    正如您在本例中看到的,一旦发生替换,PHP将从一开始就开始迭代另一个实体。好的。PHP 7哈希表迭代器

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

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

    这意味着foreach将不再使用IAP.forEach循环对current()等的结果绝对没有影响,其自身的行为也不会受到reset()等函数的影响。好的。阵列复制

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    $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中,这种特殊情况已经不复存在:数组的逐值迭代将始终处理原始元素,而忽略循环期间的任何修改。好的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    $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()完全不受循环的影响,所以其返回值始终保持不变。好的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    $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)
    "
    ;
        }
    }

    // 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的两个循环的交叉污染。好的。

    另一个现在已经修复的奇怪的边缘情况是,当您删除和添加恰好具有相同哈希值的元素时,会产生奇怪的效果:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    $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

    以前,hash pointer恢复机制直接跳转到新元素,因为它"看起来"与删除的元素相同(由于哈希和指针冲突)。因为我们不再依赖元素散列来处理任何事情,所以这不再是一个问题。好的。好啊。


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

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

    下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $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三元运算符:fast或not?


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

    a)foreach工作于原始阵列的浏览副本。这意味着foreach()将具有共享数据存储,直到或除非prospected copy是未创建foreach notes/user comments。

    b)是什么触发了预期副本?浏览副本是根据copy-on-write的策略创建的,也就是说,每当将更改传递给foreach()的数组,并创建原始数组的克隆。

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

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

    下面的示例显示,每个()和reset()都不会影响SENTINEL变量.forEach()迭代器的(for example, the current index variable)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $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/>";

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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


    PHP 7注释

    更新这个答案,因为它已经得到了一些流行:这个答案不再适用于php 7。如"向后不兼容的更改"中所述,在php 7中,foreach处理数组的副本,因此数组本身的任何更改都不会反映在foreach循环中。链接上有更多详细信息。

    解释(引自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).

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

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

    我认为这都是文档中解释的每个迭代部分的结果,这可能意味着foreach在调用{}中的代码之前就完成了所有逻辑。

    测试用例

    如果运行此命令:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?
        $array = Array(
            'foo' => 1,
            'bar' => 2
        );
        foreach($array as $k=>&$v) {
            $array['baz']=3;
            echo $v."";
        }
        print_r($array);
    ?>

    您将得到这个输出:

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?
        $array = Array(
            'foo' => 1,
            'bar' => 2
        );
        foreach($array as $k=>&$v) {
            if ($k=='bar') {
                $array['baz']=3;
            }
            echo $v."";
        }
        print_r($array);
    ?>

    你会得到:

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

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

    详细的解释可以通过php"foreach"的实际工作方式来阅读?这就解释了这种行为背后的本质。


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

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

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

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

    但在第二个例子中:

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

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


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

    1
    2
    3
    4
    5
    $set = array("apple","banana","coconut");
    foreach ( $set AS $item ) {
        echo"{$item}
    "
    ;
    }

    此输出:

    1
    2
    3
    apple
    banana
    coconut

    因此创建了副本,但开发人员没有注意到,因为循环中或循环结束后没有引用原始数组。但是,当您尝试修改循环中的项时,您会发现在完成以下操作时它们是未修改的:

    1
    2
    3
    4
    5
    6
    $set = array("apple","banana","coconut");
    foreach ( $set AS $item ) {
        $item = strrev ($item);
    }

    print_r($set);

    此输出:

    1
    2
    3
    4
    5
    6
    Array
    (
        [0] => apple
        [1] => banana
        [2] => coconut
    )

    任何对原版的更改都不能被注意到,事实上,即使您清楚地为$item指定了一个值,也没有对原版的更改。这是因为您操作的是$item,它出现在正在处理的$set的副本中。您可以通过引用获取$item来覆盖它,如下所示:

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

    此输出:

    1
    2
    3
    4
    5
    6
    Array
    (
        [0] => elppa
        [1] => ananab
        [2] => tunococ
    )

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

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

    此输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Array
    (
        [0] => apple
        [1] => banana
        [2] => coconut
        [3] => Apple
        [4] => Banana
        [5] => Coconut
    )

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

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

    1
    2
    3
    4
    5
    $set = array("apple","banana","coconut");
    foreach ( $set AS &$item ) {
        $set[] = ucfirst($item);
    }
    print_r($set);

    导致无限循环。注意,这实际上是一个无限循环,您要么自己杀死脚本,要么等待操作系统耗尽内存。我在脚本中添加了以下行,这样PHP就可以很快用完内存,如果要运行这些无限循环测试,我建议您也这样做:

    1
    ini_set("memory_limit","1M");

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


    php foreach循环可以与Indexed arraysAssociative arraysObject public variables一起使用。

    在foreach循环中,php首先要做的是创建一个数组副本,该数组将被迭代。然后,php迭代这个新的数组的copy,而不是原来的数组。这在下面的示例中演示:

    1
    2
    3
    <?php
    $numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array
    echo '[cc lang="php"]', print_r($numbers, true), '

    ,'


    ';foreach($numbers as$index=>number){$numbers[$index]=$number+1;这将更改原始数组echo'在数组的内部='$index,':',$number,'
    ';显示复制数组中的数据}echo'


    ','

    1
    ', print_r($numbers, true), '

    ;显示原始值(还包括新添加的值)。< /代码>

    除此之外,PHP还允许使用iterated values as a reference to the original array value。如下所示:

    [cc lang="php"] &$number){
    ++$number; # we are incrementing the original value
    echo 'Inside of the array = ', $index, ': ', $number, '
    '; # this is showing the original value
    }
    echo '


    ';
    echo 'ZZU1〔1〕'; # we are again showing the original value
    [cc lang="php"]

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

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