PHP'foreach'如何实际工作?

时间:2012-04-07 19:33:58

标签: php loops foreach iteration php-internals

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


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

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

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

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

这清楚地表明我们没有直接使用源数组 - 否则循环将永远持续,因为我们在循环期间不断将项目推送到数组。但只是为了确保这种情况:

Test case 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依赖于源数组的数组指针。但我们刚刚证明我们没有使用源数组,对吧?嗯,不完全是。

Test case 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)
*/

因此,尽管我们没有直接使用源数组,但我们直接使用源数组指针 - 指针位于循环结尾处数组末尾的事实显示了这一点。除非这不是真的 - 如果是,那么test case 1将永远循环。

PHP手册还指出:

  

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

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

Test case 4

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

/* Output: 1 2 3 4 5 */

Test case 5

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

/* Output: 1 2 3 4 5 */

......没有那么出乎意料的事实,它实际上似乎支持“源头复制”理论。


问题

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

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

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

6 个答案:

答案 0 :(得分:107)

在示例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之外,数组在循环期间被修改,而这个额外的引用是活的。这会触发一个克隆,并解释这里发生了什么!

这是一篇关于这种写时复制行为的另一个副作用的优秀文章:The PHP Ternary Operator: Fast or not?

答案 1 :(得分:43)

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

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

b)触发预期副本的原因是什么?     根据{{​​1}}的策略创建预期副本,即,何时     传递给copy-on-write的数组发生了变化,创建了原始数组的克隆。

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

Stack Overflow问题 How to make sure the value is reset in a 'foreach' loop in PHP? 解决了您问题的案例(3,4,5)。

以下示例显示each()和reset()不会影响foreach个变量 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/>";

答案 2 :(得分:29)

PHP 7的注释

要更新这个答案,因为它已经获得了一些流行:这个答案不再适用于PHP 7.如“Backward incompatible changes”中所述,在PHP 7中,foreach可以处理数组的副本,所以任何数组本身的更改不会反映在foreach循环上。链接的更多细节。

解释(引自php.net):

  

第一个表单循环遍历array_expression给出的数组。在每一个上   迭代时,当前元素的值被赋值给$ value和   内部数组指针前进一个(所以下一个   迭代,你将看到下一个元素。)

所以,在你的第一个例子中,你只有一个元素在数组中,并且当指针被移动时,下一个元素不存在,所以在你添加新元素foreach之后,因为它已经“决定”它作为最后一个元素。

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

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

测试用例

如果你运行:

{}

您将获得此输出:

<?
    $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 ) 已经在数组的最后一个元素时修改它,所以它“决定”不再循环,即使我们添加了新元素,我们加上它“为时已晚”而且没有通过。

详细解释可以在How does PHP 'foreach' actually work?阅读,它解释了这种行为背后的内部原因。

答案 3 :(得分:14)

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

答案 4 :(得分:11)

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

答案 5 :(得分: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 indexes用作references

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