traverse - php遍历数组




PHP'foreach'如何实际工作? (5)

让我以此为前缀说我知道什么是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 */

这支持了我们的初步结论,我们正在循环中处理源数组的副本,否则我们会在循环中看到修改后的值。 但...

如果我们查看manual ,我们会发现这样的说法:

当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()each()函数会影响循环结果的情况?

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

对于内部类来说,实际的方法调用可以通过使用内部API来避免,这些API本质上只是镜像C级别的Iterator接口。

数组和平面对象的迭代显着更复杂。 首先,应该注意的是,在PHP中,“数组”是真正有序的字典,它们将按照此顺序遍历(只要您不使用类似的东西,就与插入顺序匹配)。 这与按键的自然顺序(其他语言中的列表经常工作)或者根本没有定义的顺序(其他语言的字典经常工作)是相反的。

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

到现在为止还挺好。 迭代字典不会太难,对吧? 当你意识到在迭代过程中数组/对象可以改变时,问题就开始了。 有多种方式可以发生:

  • 如果使用foreach ($arr as &$v)通过引用进行迭代,则$arr将变为引用,并且您可以在迭代过程中对其进行更改。
  • 在PHP 5中,即使按值迭代也是如此,但数组事先是一个参考: $ref =& $arr; foreach ($ref as $v) $ref =& $arr; foreach ($ref as $v)
  • 对象具有by-handle传递语义,这对于实际目的来说意味着它们的行为与引用类似。 所以在迭代过程中总是可以改变对象。

在迭代过程中允许修改的问题是当前所在元素被删除的情况。 假设你使用一个指针来跟踪你当前在哪个数组元素。 如果这个元素现在被释放,你将留下一个悬挂指针(通常导致段错误)。

解决这个问题有不同的方法。 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执行以下schenanigans:在执行循环体之前,foreach会将指向当前元素的指针以及其散列值HashPointer到每个foreach HashPointer 。 循环体运行后,如果IAP仍然存在,它将被设置回该元素。 如果元素已被移除,我们将只使用IAP当前所在的位置。 这种方案大多是有点作用的,但是你可以从中得到很多奇怪的行为,其中一些我将在下面展示。

阵列重复

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

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

如果数组不重复(is_ref = 0,refcount = 1),那么只有它的引用计数会增加(*)。 此外,如果使用通过引用的foreach,那么(可能重复的)数组将被转换为引用。

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

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

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

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

例子:你的测试用例

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

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

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

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

  • 在测试用例4和5中, each都是引用函数。 $array在被传递给它们时有一个refcount=2 ,所以它必须被复制。 因为这样的foreach将再次在单独的数组上工作。

示例:foreach中的current效果

显示各种重复行为的好方法是观察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为1_ref = 1,所以基本上你的情况与上面相同。

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

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

这里$array循环开始时$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!

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,3,4。 如何发生的是'FYFY'与被删除的元素'FYFY'具有相同的散列,并且分配器恰好重复使用相同的内存位置来存储元素。 所以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循环期间被修改,那么会发生复制(根据copy-on-write),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()在不同参考/ 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恢复机制直接跳转到新元素,因为它“看起来”像移除元素一样(由于碰撞散列和指针)。 由于我们不再依赖元素散列来完成任何事情,这已不再是问题。


解释(从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
)

Which means that array was modified, but since we modified it when the foreach already was at the last element of the array, it "decided" not to loop anymore, and even though we added new element, we added it "too late" and it was not looped through.

Detailed explanation can be read at How does PHP 'foreach' actually work? which explains the internals behind this behaviour.


Great question, because many developers, even experienced ones, are confused by the way PHP handles arrays in foreach loops. In the standard foreach loop, PHP makes a copy of the array that is used in the loop. The copy is discarded immediately after the loop finishes. This is transparent in the operation of a simple foreach loop. 例如:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    echo "{$item}\n";
}

This outputs:

apple
banana
coconut

So the copy is created but the developer doesn't notice, because the original array isn't referenced within the loop or after the loop finishes. However, when you attempt to modify the items in a loop, you find that they are unmodified when you finish:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $item = strrev ($item);
}

print_r($set);

This outputs:

Array
(
    [0] => apple
    [1] => banana
    [2] => coconut
)

Any changes from the original can't be notices, actually there are no changes from the original, even though you clearly assigned a value to $item. This is because you are operating on $item as it appears in the copy of $set being worked on. You can override this by grabbing $item by reference, like so:

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

This outputs:

Array
(
    [0] => elppa
    [1] => ananab
    [2] => tunococ
)

So it is evident and observable, when $item is operated on by-reference, the changes made to $item are made to the members of the original $set. Using $item by reference also prevents PHP from creating the array copy. To test this, first we'll show a quick script demonstrating the copy:

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

This outputs:

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

As it is shown in the example, PHP copied $set and used it to loop over, but when $set was used inside the loop, PHP added the variables to the original array, not the copied array. Basically, PHP is only using the copied array for the execution of the loop and the assignment of $item. Because of this, the loop above only executes 3 times, and each time it appends another value to the end of the original $set, leaving the original $set with 6 elements, but never entering an infinite loop.

However, what if we had used $item by reference, as I mentioned before? A single character added to the above test:

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

Results in an infinite loop. Note this actually is an infinite loop, you'll have to either kill the script yourself or wait for your OS to run out of memory. I added the following line to my script so PHP would run out of memory very quickly, I suggest you do the same if you're going to be running these infinite loop tests:

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

So in this previous example with the infinite loop, we see the reason why PHP was written to create a copy of the array to loop over. When a copy is created and used only by the structure of the loop construct itself, the array stays static throughout the execution of the loop, so you'll never run into issues.


PHP foreach loop can be used with Indexed arrays , Associative arrays and Object public variables .

In foreach loop, the first thing php does is that it creates a copy of the array which is to be iterated over. PHP then iterates over this new copy of the array rather than the original one. This is demonstrated in the below example:

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

Besides this, php does allow to use iterated values as a reference to the original array value as well. This is demonstrated below:

<?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

Note: It does not allow original array indexes to be used as references .

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


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