让我先说一下,我知道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使用数组的副本,但在循环后将源数组的数组指针设置到数组的末尾。

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


当前回答

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

a) foreach处理原始阵列的预测副本。这意味着foreach()将具有SHARED数据存储,除非未创建foreach Notes/User注释。

b) 是什么触发了预期拷贝?根据写时复制策略,即每当传递给foreach()的数组被更改,原始数组的克隆被创建。

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

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

以下示例显示each()和reset()不影响SENTINEL变量(例如,当前索引变量)。

$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

其他回答

PHP foreach循环可以与索引数组、关联数组和Object公共变量一起使用。

在foreach循环中,php所做的第一件事是创建要迭代的数组的副本。然后PHP迭代数组的这个新副本,而不是原始副本。下面的示例演示了这一点:

<?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还允许使用迭代值作为原始数组值的引用。如下所示:

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

注意:它不允许将原始数组索引用作引用。

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

php7注释

更新这个答案,因为它已经获得了一些流行:这个答案从PHP7开始不再适用。正如在“向后不兼容的更改”中所解释的,在PHP7中,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”实际上是如何工作的?这解释了这种行为背后的内在原因。

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

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

a) foreach处理原始阵列的预测副本。这意味着foreach()将具有SHARED数据存储,除非未创建foreach Notes/User注释。

b) 是什么触发了预期拷贝?根据写时复制策略,即每当传递给foreach()的数组被更改,原始数组的克隆被创建。

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

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

以下示例显示each()和reset()不影响SENTINEL变量(例如,当前索引变量)。

$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

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