PHP'foreach'如何实际工作?

DaveRandom 1926 php iteration foreach loops php-internals

让我通过说我知道它是什么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与数组的副本一起工作,但在循环之后将源数组的数组指针设置为数组的末尾.

  • 这是正确的和整个故事吗?
  • 如果没有,它到底在做什么?
  • 是否有一个地方使用调整数组指针函数(任何情况下each(),reset()等)过程中foreach可能会影响循环的结果?

NikiC.. 1597

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

对于内部类,通过使用基本上只镜像IteratorC级接口的内部API来避免实际的方法调用.

数组和普通对象的迭代要复杂得多.首先,应该注意的是,PHP中的"数组"实际上是有序的字典,它们将按照这个顺序遍历(只要你没有使用类似的东西就匹配插入顺序sort).这与按键的自然顺序(其他语言中的列表通常如何工作)或根本没有定义的顺序(其他语言中的字典通常如何工作)相反.

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

到现在为止还挺好.迭代字典不能太难,对吧?当您意识到数组/对象在迭代期间可以更改时,问题就开始了.有多种方法可以实现:

  • 如果您通过引用迭代使用foreach ($arr as &$v)然后$arr转换为引用,您可以在迭代期间更改它.
  • 在PHP 5中,即使您按值迭代,同样适用,但数组事先是一个引用: $ref =& $arr; foreach ($ref as $v)
  • 对象具有处理传递语义的功能,对于大多数实际用途而言,它们的行为类似于引用.因此,在迭代期间总是可以更改对象.

在迭代期间允许修改的问题是删除当前所在元素的情况.假设您使用指针来跟踪您当前所在的数组元素.如果现在释放了此元素,则会留下悬空指针(通常会导致段错误).

有不同的方法来解决这个问题.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将指向当前元素及其散列的指针为per-foreach foreach.循环体运行后,如果IAP仍然存在,IAP将被设置回该元素.但是,如果元素已被删除,我们将只在IAP当前所在的位置使用.这个方案主要是有点类型的工作,但是你可以从中获得很多奇怪的行为,其中一些我将在下面演示.

数组重复

IAP是数组的可见特征(通过foreach函数族公开),因为IAP计数的这些更改是在写时复制语义下的修改.遗憾的是,这意味着foreach在许多情况下被迫复制它正在迭代的数组.确切的条件是:

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

如果数组没有重复(is_ref = 0,refcount = 1),那么只有它的引用计数会递增(*).此外,如果使用foreach by reference,则(可能重复的)数组将变为引用.

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

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

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

在这里,foreach将重复以防止IAP更改foreach泄漏到HashPointer.就上述条件而言,数组不是引用(is_ref = 0),并且在两个地方使用(refcount = 2).这个要求是不幸的,也是次优实现的工件(这里不需要修改迭代,因此我们不需要首先使用IAP).

(*) Incrementing the refcount here sounds innocuous, but violates copy-on-write (COW) semantics: This means that we are going to modify the IAP of a refcount=2 array, while COW dictates that modifications can only be performed on refcount=1 values. This violation results in user-visible behavior change (while a COW is normally transparent) because the IAP change on the iterated array will be observable -- but only until the first non-IAP modification on the array. Instead, the three "valid" options would have been a) to always duplicate, b) to not increment the refcount and thus allowing the iterated array to be arbitrarily modified in the loop or c) don't use the IAP at all (the PHP 7 solution).

Position advancement order

There is one last implementation detail that you have to be aware of to properly understand the code samples below. The "normal" way of looping through some data structure would look something like this in pseudocode:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

However current, being a rather special snowflake, chooses to do things slightly differently:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

Namely, the array pointer is already moved forward before the loop body runs. This means that while the loop body is working on element foreach, the IAP is already at element refcount. This is the reason why code samples showing modification during iteration will always unset the next element, rather than the current one.

Examples: Your test cases

The three aspects described above should provide you with a mostly complete impression of the idiosyncrasies of the foreach implementation and we can move on to discuss some examples.

The behavior of your test cases is simple to explain at this point:

  • In test cases 1 and 2 refcount starts off with refcount=1, so it will not be duplicated by foreach: Only the refcount is incremented. When the loop body subsequently modifies the array (which has refcount=2 at that point), the duplication will occur at that point. Foreach will continue working on an unmodified copy of foreach.

  • In test case 3, once again the array is not duplicated, thus foreach will be modifying the IAP of the $arr variable. At the end of the iteration, the IAP is NULL (meaning iteration has done), which $arr indicates by returning $outerArr.

  • In test cases 4 and 5 both refcount and refcount are by-reference functions. The foreach has a $i when it is passed to them, so it has to be duplicated. As such $i+1 will be working on a separate array again.

Examples: Effects of unset in foreach

A good way to show the various duplication behaviors is to observe the behavior of the foreach function inside a foreach loop. Consider this example:

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

Here you should know that $array is a by-ref function (actually: prefer-ref), even though it does not modify the array. It has to be in order to play nice with all the other functions like foreach which are all by-ref. By-reference passing implies that the array has to be separated and thus refcount and the foreach-array will be different. The reason you get $array instead of foreach is also mentioned above: $array advances the array pointer before running the user code, not after. So even though the code is at the first element, foreach already advanced the pointer to the second.

Now lets try a small modification:

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

Here we have the is_ref=1 case, so the array is not copied (just like above). But now that it is a reference, the array no longer has to be duplicated when passing to the by-ref each function. Thus false and foreach work on the same array. You still see the off-by-one behavior though, due to the way each advances the pointer.

You get the same behavior when doing by-ref iteration:

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

Here the important part is that foreach will make reset an is_ref=1 when it is iterated by reference, so basically you have the same situation as above.

Another small variation, this time we'll assign the array to another variable:

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

Here the refcount of the $array is 2 when the loop is started, so for once we actually have to do the duplication upfront. Thus refcount=2 and the array used by foreach will be completely separate from the outset. That's why you get the position of the IAP wherever it was before the loop (in this case it was at the first position).

Examples: Modification during iteration

Trying to account for modifications during iteration is where all our foreach troubles originated, so it serves to consider some examples for this case.

Consider these nested loops over the same array (where by-ref iteration is used to make sure it really is the same one):

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)

The expected part here is that foreach is missing from the output because element current was removed. What's probably unexpected is that the outer loop stops after the first element. Why is that?

The reason behind this is the nested-loop hack described above: Before the loop body runs, the current IAP position and hash is backed up into a current(). After the loop body it will be restored, but only if the element still exists, otherwise the current IAP position (whatever it may be) is used instead. In the example above this is exactly the case: The current element of the outer loop has been removed, so it will use the IAP, which has already been marked as finished by the inner loop!

Another consequence of the foreach backup+restore mechanism is that changes to the IAP though current() etc. usually do not impact foreach. For example, the following code executes as if the next were not present at all:

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

The reason is that, while $array temporarily modifies the IAP, it will be restored to the current foreach element after the loop body. To force foreach-array to make an effect on the loop, you have to additionally remove the current element, so that the backup/restore mechanism fails:

$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

But, those examples are still sane. The real fun starts if you remember that the 2 restore uses a pointer to the element and its hash to determine whether it still exists. But: Hashes have collisions, and pointers can be reused! This means that, with a careful choice of array keys, we can make 1 believe that an element that has been removed still exists, so it will jump directly to it. An example:

$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

Here we should normally expect the output foreach according to the previous rules. How what happens is that foreach has the same hash as the removed element current(), and the allocator happens to reuse the same memory location to store the element. So foreach ends up directly jumping to the newly inserted element, thus short-cutting the loop.

Substituting the iterated entity during the loop

One last odd case that I'd like to mention, it is that PHP allows you to substitute the iterated entity during the loop. So you can start iterating on one array and then replace it with another array halfway through. Or start iterating on an array and then replace it with an object:

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

As you can see in this case PHP will just start iterating the other entity from the start once the substitution has happened.

PHP 7

Hashtable iterators

If you still remember, the main problem with array iteration was how to handle removal of elements mid-iteration. PHP 5 used a single internal array pointer (IAP) for this purpose, which was somewhat suboptimal, as one array pointer had to be stretched to support multiple simultaneous foreach loops and interaction with current() etc. on top of that.

PHP 7 uses a different approach, namely, it supports creating an arbitrary amount of external, safe hashtable iterators. These iterators have to be registered in the array, from which point on they have the same semantics as the IAP: If an array element is removed, all hashtable iterators pointing to that element will be advanced to the next element.

This means that foreach will no longer use the IAP at all. The foreach loop will be absolutely no effect on the results of foreach etc. and its own behavior will never be influenced by functions like foreach etc.

Array duplication

Another important change between PHP 5 and PHP 7 relates to array duplication. Now that the IAP is no longer used, by-value array iteration will only do a refcount increment (instead of duplication the array) in all cases. If the array is modified during the foreach loop, at that point a duplication will occur (according to copy-on-write) and foreach will keep working on the old array.

In most cases, this change is transparent and has no other effect than better performance. However, there is one occasion where it results in different behavior, namely the case where the array was a reference beforehand:

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

Previously by-value iteration of reference-arrays was special cases. In this case, no duplication occurred, so all modifications of the array during iteration would be reflected by the loop. In PHP 7 this special case is gone: A by-value iteration of an array will always keep working on the original elements, disregarding any modifications during the loop.

This, of course, does not apply to by-reference iteration. If you iterate by-reference all modifications will be reflected by the loop. Interestingly, the same is true for by-value iteration of plain objects:

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

This reflects the by-handle semantics of objects (i.e. they behave reference-like even in by-value contexts).

Examples

Let's consider a few examples, starting with your test cases:

  • Test cases 1 and 2 retain the same output: By-value array iteration always keep working on the original elements. (In this case, even refcounting and duplication behavior is exactly the same between PHP 5 and PHP 7).

  • Test case 3 changes: Foreach no longer uses the IAP, so $array is not affected by the loop. It will have the same output before and after.

  • Test cases 4 and 5 stay the same: $array and $array will duplicate the array before changing the IAP, while foreach still uses the original array. (Not that the IAP change would have mattered, even if the array was shared.)

The second set of examples was related to the behavior of (1, 2) under different reference/refcounting configurations. This no longer makes sense, as 1 is completely unaffected by the loop, so its return value always stays the same.

However, we get some interesting changes when considering modifications during iteration. I hope you will find the new behavior saner. The first example:

$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) 

As you can see, the outer loop no longer aborts after the first iteration. The reason is that both loops now have entirely separate hashtable iterators, and there is no longer any cross-contamination of both loops through a shared IAP.

Another weird edge case that is fixed now, is the odd effect you get when you remove and add elements that happen to have the same hash:

$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

Previously the HashPointer restore mechanism jumped right to the new element because it "looked" like it's the same as the removed element (due to colliding hash and pointer). As we no longer rely on the element hash for anything, this is no longer an issue.

  • 对于那些不知道zval是什么的人,请参阅Sara Goleman的http://blog.golemon.com/2007/01/youre-being-lied-to.html (32认同)
  • @unbeli我正在使用PHP内部使用的术语.`Bucket`s是哈希冲突的双向链表的一部分,也是订单双向链表的一部分;) (11认同)
  • @Baba确实如此.将它传递给函数与在循环之前执行`$ foo = $ array`相同;) (4认同)
  • 伟大的anwser.我认为你的意思是`iterate($ outerArr);`而不是`iterate($ arr);`某处. (4认同)

linepogl.. 111

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


sakhunzai.. 44

使用时需注意以下几点foreach():

a)处理原始数组foreach预期副本.这意味着foreach()将具有SHARED数据存储,直到或除非foreach()未创建Notes/User注释.

b)触发预期副本的原因是什么?根据策略创建预期副本prospected copy,即,每当传递给foreach()的数组发生更改时,都会创建原始数组的克隆.

c)原始数组和foreach()迭代器将具有copy-on-write一个用于原始数组,另一个用于foreach; 请参阅下面的测试代码.SPL,IteratorsArray Iterator.

Stack Overflow问题如何确保在PHP的'foreach'循环中重置该值?解决你问题的案例(3,4,5).

以下示例显示each()和reset()不会影响foreach()迭代器的foreach()变量 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

  • 你的答案不太正确.`foreach`对数组的潜在副本进行操作,但除非需要,否则它不会生成实际的副本. (2认同)

Damir Kasipo.. 31

PHP 7的注释

要更新这个答案,因为它已经获得了一些普及:这个答案不再适用于PHP 7.正如在" 向后不兼容的更改 "中所解释的那样,在PHP 7中,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'如何实际工作中阅读?这解释了这种行为背后的内部.

  • 你读完其余的答案了吗?完全有道理,foreach决定它是否会在**之前循环****它甚至会运行其中的代码. (7认同)
  • 实际上似乎@AlmaDo在理解自己的逻辑方面存在缺陷......你的答案很好. (4认同)
  • 不,数组被修改,但"太晚了",因为foreach已经"认为"它处于最后一个元素(它在迭代开始时)并且不再循环.在第二个示例中,它不在迭代开始时的最后一个元素,并在下一次迭代的开始时再次计算.我正在尝试准备一个测试用例. (2认同)

小智.. 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;通过引用添加.


小智.. 12

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


小智.. 8

PHP foreach循环可以与使用Indexed arrays,Associative arrays以及Object 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


归档时间:

查看次数:

392057 次

最近记录:

10 月,2 周 前