天天看點

PHP“ foreach”實際上如何工作?

本文翻譯自:How does PHP 'foreach' actually work?

Let me prefix this by saying that I know what

foreach

is, does and how to use it.

首先,我要說一下我知道

foreach

是什麼,做什麼以及如何使用它。

This question concerns how it works under the bonnet, and I don't want any answers along the lines of "this is how you loop an array with

foreach

".

這個問題涉及它在引擎蓋下的工作方式,我不希望“這就是使用

foreach

循環數組的方式”的答案。

For a long time I assumed that

foreach

worked with the array itself.

很長時間以來,我一直認為

foreach

與數組本身一起工作。

Then I found many references to the fact that it works with a copy of the array, and I have since assumed this to be the end of the story.

然後,我發現了很多關于它可以與數組副本一起使用的事實的引用,從那時起,我一直以為這是故事的結尾。

But I recently got into a discussion on the matter, and after a little experimentation found that this was not in fact 100% true.

但是我最近對此事進行了讨論,經過一番實驗後發現這實際上并非100%正确。

Let me show what I mean.

讓我表明我的意思。

For the following test cases, we will be working with the following array:

對于以下測試用例,我們将使用以下數組:
$array = array(1, 2, 3, 4, 5);
           

Test case 1 :

測試用例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 */
           

This clearly shows that we are not working directly with the source array - otherwise the loop would continue forever, since we are constantly pushing items onto the array during the loop.

這清楚地表明,我們不是直接使用源數組-否則循環将永遠持續下去,因為在循環過程中我們不斷将項目推入數組。

But just to be sure this is the case:

但是隻是為了確定是這種情況:

Test case 2 :

測試用例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 */
           

This backs up our initial conclusion, we are working with a copy of the source array during the loop, otherwise we would see the modified values during the loop.

這支援了我們的初始結論,我們在循環期間正在使用源數組的副本,否則将在循環期間看到修改後的值。

But...

但...

If we look in the manual , we find this statement:

如果檢視手冊 ,則會發現以下語句:
When foreach first starts executing, the internal array pointer is automatically reset to the first element of the array. 當foreach首先開始執行時,内部數組指針将自動重置為數組的第一個元素。

Right... this seems to suggest that

foreach

relies on the array pointer of the source array.

對...這似乎表明,

foreach

依賴于源數組的數組指針。

But we've just proved that we're not working with the source array , right?

但是我們剛剛證明我們沒有使用源數組 ,對嗎?

Well, not entirely.

好吧,不完全是。

Test case 3 :

測試用例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)
*/
           

So, despite the fact that we are not working directly with the source array, we are working directly with the source array pointer - the fact that the pointer is at the end of the array at the end of the loop shows this.

是以,盡管事實上我們并沒有直接使用源數組,而是直接使用了源數組指針-指針在循環末尾位于數組的末尾這一事實表明了這一點。

Except this can't be true - if it was, then test case 1 would loop forever.

除非這不能成立-如果是,那麼測試用例1将永遠循環。

The PHP manual also states:

PHP手冊還指出:
As foreach relies on the internal array pointer changing it within the loop may lead to unexpected behavior. 由于foreach依賴内部數組指針,是以在循環内更改它可能導緻意外行為。

Well, let's find out what that "unexpected behavior" is (technically, any behavior is unexpected since I no longer know what to expect).

好吧,讓我們找出“意外行為”是什麼(從技術上講,任何行為都是意外的,因為我不再知道會發生什麼)。

Test case 4 :

測試用例4 :
foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */
           

Test case 5 :

測試用例5 :
foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */
           

...nothing that unexpected there, in fact it seems to support the "copy of source" theory.

...沒有什麼意外的,實際上似乎支援“源代碼複制”理論。

The Question

問題

What is going on here?

這裡發生了什麼?

My C-fu is not good enough for me to able to extract a proper conclusion simply by looking at the PHP source code, I would appreciate it if someone could translate it into English for me.

我的C-fu不足以讓我僅通過檢視PHP源代碼就能得出正确的結論,如果有人可以為我翻譯成英語,我将不勝感激。

It seems to me that

foreach

works with a copy of the array, but sets the array pointer of the source array to the end of the array after the loop.

在我看來,

foreach

使用數組的副本 ,但是将循環後的源數組的數組指針設定為數組的末尾。
  • Is this correct and the whole story? 這是正确的故事嗎?
  • If not, what is it really doing? 如果沒有,它到底在做什麼?
  • Is there any situation where using functions that adjust the array pointer (

    each()

    ,

    reset()

    et al.) during a

    foreach

    could affect the outcome of the loop? 是否有任何情況在

    foreach

    期間使用調整數組指針的函數(

    each()

    reset()

    等)會影響循環的結果?

#1樓

參考:https://stackoom.com/question/gCSV/PHP-foreach-實際上如何工作

#2樓

In example 3 you don't modify the array.

在示例3中,您無需修改​​數組。

In all other examples you modify either the contents or the internal array pointer.

在所有其他示例中,您将修改内容或内部數組指針。

This is important when it comes to PHP arrays because of the semantics of the assignment operator.

對于PHP數組,這很重要,因為指派運算符具有語義。

The assignment operator for the arrays in PHP works more like a lazy clone.

PHP中數組的指派運算符的工作方式更像是惰性克隆。

Assigning one variable to another that contains an array will clone the array, unlike most languages.

與大多數語言不同,将一個變量配置設定給另一個包含數組的變量将克隆該數組。

However, the actual cloning will not be done unless it is needed.

但是,除非需要,否則不會進行實際的克隆。

This means that the clone will take place only when either of the variables is modified (copy-on-write).

這意味着僅當修改了兩個變量(寫時複制)時,才會進行克隆。

Here is an example:

這是一個例子:
$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.
           

Coming back to your test cases, you can easily imagine that

foreach

creates some kind of iterator with a reference to the array.

回到測試用例,您可以輕松地想象到

foreach

通過引用數組建立了某種疊代器。

This reference works exactly like the variable

$b

in my example.

此引用的工作方式與示例中的變量

$b

完全相同。

However, the iterator along with the reference live only during the loop and then, they are both discarded.

但是,疊代器和引用僅在循環期間存在,然後都被丢棄。

Now you can see that, in all cases but 3, the array is modified during the loop, while this extra reference is alive.

現在您可以看到,在除3之外的所有情況下,在循環期間都将修改數組,而該額外引用仍然有效。

This triggers a clone, and that explains what's going on here!

這會觸發一個克隆,這說明了這裡發生了什麼!

Here is an excellent article for another side effect of this copy-on-write behaviour: The PHP Ternary Operator: Fast or not?

這是一篇出色的文章,說明了這種寫時複制行為的另一個副作用: PHP三元運算符:速度快還是快?

#3樓

Some points to note when working with

foreach()

:

使用

foreach()

時需要注意的幾點:

a)

foreach

works on the prospected copy of the original array.

a)

foreach

在原始數組的預期副本上工作。

It means

foreach()

will have SHARED data storage until or unless a

prospected copy

is not created foreach Notes/User comments .

這意味着

foreach()

将具有共享的資料存儲,直到或除非未為foreach Notes / User comments建立

prospected copy

b) What triggers a prospected copy ?

b)是什麼觸發了預期複制 ?

A prospected copy is created based on the policy of

copy-on-write

, that is, whenever an array passed to

foreach()

is changed, a clone of the original array is created.

根據

copy-on-write

copy-on-write

的政策建立預期的副本,也就是說,每當更改傳遞給

foreach()

的數組時,都會建立原始數組的副本。

c) The original array and

foreach()

iterator will have

DISTINCT SENTINEL VARIABLES

, that is, one for the original array and other for

foreach

;

c)原始數組和

foreach()

疊代器将具有

DISTINCT SENTINEL VARIABLES

,即,一個用于原始數組,另一個用于

foreach

see the test code below.

請參閱下面的測試代碼。

SPL , Iterators , and Array Iterator .

SPL , 疊代器和數組疊代器 。

Stack Overflow question How to make sure the value is reset in a 'foreach' loop in PHP?

堆棧溢出問題如何確定在PHP的“ foreach”循環中重置該值?

addresses the cases (3,4,5) of your question.

解決您的問題的情況(3,4,5)。

The following example shows that each() and reset() DOES NOT affect

SENTINEL

variables

(for example, the current index variable)

of the

foreach()

iterator.

下面的示例顯示each()和reset()不會影響

foreach()

疊代器的

SENTINEL

變量

(for example, the current index variable)

$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/>";
           

Output:

輸出:
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
           

#4樓

foreach

supports iteration over three different kinds of values:

foreach

支援對三種不同類型的值進行疊代:
  • Arrays 數組
  • Normal objects 普通物體
  • Traversable

    objects

    Traversable

    對象

In the following, I will try to explain precisely how iteration works in different cases.

在下文中,我将嘗試精确解釋疊代在不同情況下如何工作。

By far the simplest case is

Traversable

objects, as for these

foreach

is essentially only syntax sugar for code along these lines:

到目前為止,最簡單的情況是

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

For internal classes, actual method calls are avoided by using an internal API that essentially just mirrors the

Iterator

interface on the C level.

對于内部類,通過使用實質上隻是在C級别上鏡像

Iterator

接口的内部API,可以避免實際的方法調用。

Iteration of arrays and plain objects is significantly more complicated.

數組和普通對象的疊代要複雜得多。

First of all, it should be noted that in PHP "arrays" are really ordered dictionaries and they will be traversed according to this order (which matches the insertion order as long as you didn't use something like

sort

).

首先,應該注意的是,在PHP中,“數組”實際上是有序的字典,它們将根據此順序周遊(隻要您不使用

sort

等内容,它們就與插入順序比對)。

This is opposed to iterating by the natural order of the keys (how lists in other languages often work) or having no defined order at all (how dictionaries in other languages often work).

這與通過鍵的自然順序(其他語言的清單通常如何工作)或根本沒有定義的順序(其他語言的詞典通常如何工作)進行疊代相反。

The same also applies to objects, as the object properties can be seen as another (ordered) dictionary mapping property names to their values, plus some visibility handling.

這同樣适用于對象,因為對象屬性可以看作是另一個(有序的)字典,将屬性名稱映射到其值,再加上一些可見性處理。

In the majority of cases, the object properties are not actually stored in this rather inefficient way.

在大多數情況下,對象屬性實際上并不是以這種相當低效的方式存儲的。

However, if you start iterating over an object, the packed representation that is normally used will be converted to a real dictionary.

但是,如果您開始周遊對象,則通常使用的打包表示形式将轉換為實際字典。

At that point, iteration of plain objects becomes very similar to iteration of arrays (which is why I'm not discussing plain-object iteration much in here).

到那時,普通對象的疊代變得與數組的疊代非常相似(這就是為什麼我在這裡不讨論普通對象疊代的原因)。

So far, so good.

到現在為止還挺好。

Iterating over a dictionary can't be too hard, right?

周遊字典不會太難,對吧?

The problems begin when you realize that an array/object can change during iteration.

當您意識到數組/對象可以在疊代過程中更改時,問題就開始了。

There are multiple ways this can happen:

發生這種情況的方式有多種:
  • If you iterate by reference using

    foreach ($arr as &$v)

    then

    $arr

    is turned into a reference and you can change it during iteration. 如果使用

    foreach ($arr as &$v)

    通過引用進行疊代,則

    $arr

    将變為引用,您可以在疊代期間進行更改。
  • In PHP 5 the same applies even if you iterate by value, but the array was a reference beforehand:

    $ref =& $arr; foreach ($ref as $v)

    在PHP 5中,即使您按值進行疊代也是如此,但是該數組是預先引用的:

    $ref =& $arr; foreach ($ref as $v)

    $ref =& $arr; foreach ($ref as $v)

  • Objects have by-handle passing semantics, which for most practical purposes means that they behave like references. 對象具有按句柄傳遞的語義,對于大多數實際目的,這意味着它們的行為類似于引用。 So objects can always be changed during iteration. 是以,在疊代過程中始終可以更改對象。

The problem with allowing modifications during iteration is the case where the element you are currently on is removed.

疊代期間允許修改的問題是目前所在元素被删除的情況。

Say you use a pointer to keep track of which array element you are currently at.

假設您使用指針來跟蹤目前所在的數組元素。

If this element is now freed, you are left with a dangling pointer (usually resulting in a segfault).

如果現在釋放了此元素,則留下一個懸空的指針(通常會導緻段錯誤)。

There are different ways of solving this issue.

有多種解決此問題的方法。

PHP 5 and PHP 7 differ significantly in this regard and I'll describe both behaviors in the following.

PHP 5和PHP 7在這方面有很大不同,我将在下面描述這兩種行為。

The summary is that PHP 5's approach was rather dumb and lead to all kinds of weird edge-case issues, while PHP 7's more involved approach results in more predictable and consistent behavior.

總結是,PHP 5的方法相當笨拙,并導緻各種奇怪的極端情況問題,而PHP 7的方法更複雜,導緻行為的可預測性和一緻性更高。

As a last preliminary, it should be noted that PHP uses reference counting and copy-on-write to manage memory.

最後,應該注意的是PHP使用引用計數和寫時複制來管理記憶體。

This means that if you "copy" a value, you actually just reuse the old value and increment its reference count (refcount).

這意味着,如果您“複制”一個值,則實際上隻是重用舊值并增加其引用計數(refcount)。

Only once you perform some kind of modification a real copy (called a "duplication") will be done.

僅當您執行某種修改後,才會完成真實副本(稱為“複制”)。

See You're being lied to for a more extensive introduction on this topic.

請參閱“ 您被騙了”以擷取有關此主題的更廣泛的介紹。

PHP 5 PHP 5

Internal array pointer and HashPointer 内部數組指針和HashPointer

Arrays in PHP 5 have one dedicated "internal array pointer" (IAP), which properly supports modifications: Whenever an element is removed, there will be a check whether the IAP points to this element.

PHP 5中的數組具有一個專用的“内部數組指針”(IAP),該數組正确支援修改:每當删除元素時,都會檢查IAP是否指向該元素。

If it does, it is advanced to the next element instead.

如果是這樣,它将前進到下一個元素。

While

foreach

does make use of the IAP, there is an additional complication: There is only one IAP, but one array can be part of multiple

foreach

loops:

盡管

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) {
        // ...
    }
}
           

To support two simultaneous loops with only one internal array pointer,

foreach

performs the following shenanigans: Before the loop body is executed,

foreach

will back up a pointer to the current element and its hash into a per-foreach

HashPointer

.

為了僅使用一個内部數組指針支援兩個同時循環,

foreach

執行以下操作:在循環體執行之前,

foreach

将指向目前元素的指針及其哈希值

HashPointer

到每個

HashPointer

After the loop body runs, the IAP will be set back to this element if it still exists.

循環主體運作後,如果IAP仍然存在,則将其設定回該元素。

If however the element has been removed, we'll just use wherever the IAP is currently at.

但是,如果該元素已被删除,我們将僅使用IAP目前所在的位置。

This scheme mostly-kinda-sort of works, but there's a lot of weird behavior you can get out of it, some of which I'll demonstrate below.

這個方案主要是某種類型的作品,但是您可以擺脫很多奇怪的行為,下面将對其中的一些進行示範。

Array duplication 陣列複制

The IAP is a visible feature of an array (exposed through the

current

family of functions), as such changes to the IAP count as modifications under copy-on-write semantics.

IAP是數組的一個可見功能(通過

current

的函數系列公開),是以對IAP計數的更改是寫時複制語義下的修改。

This, unfortunately, means that

foreach

is in many cases forced to duplicate the array it is iterating over.

不幸的是,這意味着在許多情況下,

foreach

被迫複制正在疊代的數組。

The precise conditions are:

精确條件是:
  1. The array is not a reference (is_ref=0). 該數組不是引用(is_ref = 0)。 If it's a reference, then changes to it are supposed to propagate, so it should not be duplicated. 如果是參考,則應該傳播對其的更改,是以不應重複。
  2. The array has refcount>1. 數組的引用計數> 1。 If

    refcount

    is 1, then the array is not shared and we're free to modify it directly. 如果

    refcount

    為1,則不共享該數組,我們可以自由地直接對其進行修改。

If the array is not duplicated (is_ref=0, refcount=1), then only its

refcount

will be incremented (*).

如果數組不重複(is_ref = 0,refcount = 1),則僅其

refcount

将遞增(*)。

Additionally, if

foreach

by reference is used, then the (potentially duplicated) array will be turned into a reference.

此外,如果使用按引用的

foreach

,則(可能重複的)數組将變為引用。

Consider this code as an example where duplication occurs:

将此代碼視為發生重複的示例:
function iterate($arr) {
    foreach ($arr as $v) {}
}

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

Here,

$arr

will be duplicated to prevent IAP changes on

$arr

from leaking to

$outerArr

.

在這裡,

$arr

将被複制,以防止

$arr

上的IAP更改洩漏到

$outerArr

In terms of the conditions above, the array is not a reference (is_ref=0) and is used in two places (refcount=2).

根據上述條件,該數組不是引用(is_ref = 0),并且在兩個地方使用(refcount = 2)。

This requirement is unfortunate and an artifact of the suboptimal implementation (there is no concern of modification during iteration here, so we don't really need to use the IAP in the first place).

不幸的是,此要求是次佳實作的産物(這裡沒有考慮疊代過程中的修改,是以我們實際上并不需要首先使用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.

(*)在此處增加

refcount

聽起來無害,但違反了寫時複制(COW)語義:這意味着我們将修改refcount = 2數組的IAP,而COW訓示隻能在refcount上執行修改= 1個值。

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.

違反行為會導緻使用者可見的行為更改(而COW通常是透明的),因為可以觀察到疊代陣列上的IAP更改-但僅在陣列上第一次進行非IAP修改之前。

Instead, the three "valid" options would have been a) to always duplicate, b) do 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).

取而代之的是,三個“有效”選項是:a)始終重複,b)不增加

refcount

,是以允許在循環中随意修改疊代數組,或者c)根本不使用IAP( PHP 7解決方案)。

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

foreach

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

但是,

foreach

是一個非常特殊的雪花,它選擇做的事情略有不同:
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

$i

, the IAP is already at element

$i+1

.

這意味着,當循環體在元素

$i

上工作時,IAP已在元素

$i+1

This is the reason why code samples showing modification during iteration will always

unset

the next element, rather than the current one.

這就是為什麼在疊代過程中顯示修改的代碼示例将始終

unset

下一個元素而不是目前元素的原因。

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.

上面描述的三個方面應該為您大緻了解一下

foreach

實作的特質,我們可以繼續讨論一些示例。

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

此時,測試用例的行為很容易解釋:
  • In test cases 1 and 2

    $array

    starts off with refcount=1, so it will not be duplicated by

    foreach

    : Only the

    refcount

    is incremented. 在測試用例1和2中,

    $array

    從refcount = 1開始,是以它不會被

    foreach

    複制:僅增加

    refcount

    When the loop body subsequently modifies the array (which has refcount=2 at that point), the duplication will occur at that point. 當循環主體随後修改數組時(此時refcount = 2),複制将在該點進行。 Foreach will continue working on an unmodified copy of

    $array

    . Foreach将繼續處理

    $array

    的未修改副本。
  • In test case 3, once again the array is not duplicated, thus

    foreach

    will be modifying the IAP of the

    $array

    variable. 在測試用例3中,數組不再重複,是以

    foreach

    将修改

    $array

    變量的IAP。
    At the end of the iteration, the IAP is NULL (meaning iteration has done), which

    each

    indicates by returning

    false

    . 在疊代結束時,IAP為NULL(表示疊代已完成),

    each

    通過傳回

    false

    訓示。
  • In test cases 4 and 5 both

    each

    and

    reset

    are by-reference functions. 在測試用例4和5中,

    each

    reset

    都是參考功能。
    The

    $array

    has a

    refcount=2

    when it is passed to them, so it has to be duplicated.

    $array

    傳遞給他們時具有

    refcount=2

    ,是以必須重複。
    As such

    foreach

    will be working on a separate array again. 這樣,

    foreach

    将再次在單獨的數組上工作。

Examples: Effects of

current

in foreach 示例:foreach中的

current

影響

A good way to show the various duplication behaviors is to observe the behavior of the

current()

function inside a

foreach

loop.

顯示各種複制行為的一個好方法是觀察

foreach

循環中

current()

函數的行為。

Consider this example:

考慮以下示例:
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */
           

Here you should know that

current()

is a by-ref function (actually: prefer-ref), even though it does not modify the array.

在這裡,您應該知道

current()

是一個by-ref函數(實際上是:referr-ref),即使它不修改數組也是如此。

It has to be in order to play nice with all the other functions like

next

which are all by-ref.

它必須是為了與所有其他功能(如

next

,它們都是by-ref。

By-reference passing implies that the array has to be separated and thus

$array

and the

foreach-array

will be different.

通過引用傳遞意味着必須将數組分開,是以

$array

foreach-array

将不同。

The reason you get

2

instead of

1

is also mentioned above:

foreach

advances the array pointer before running the user code, not after.

上面還提到了

2

而不是

1

的原因:

foreach

在運作使用者代碼之前 (而不是之後)推進數組指針。

So even though the code is at the first element,

foreach

already advanced the pointer to the second.

是以,即使代碼位于第一個元素,

foreach

已将指針前進到第二個元素。

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

這裡有is_ref = 1的情況,是以不複制數組(就像上面一樣)。

But now that it is a reference, the array no longer has to be duplicated when passing to the by-ref

current()

function.

但是,既然它是一個引用,則在傳遞給by-ref

current()

函數時,不再需要複制該數組。

Thus

current()

and

foreach

work on the same array.

是以,

current()

foreach

在同一數組上工作。

You still see the off-by-one behavior though, due to the way

foreach

advances the pointer.

但是,由于

foreach

前進指針的方式,您仍然會看到偏離行為。

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

在進行by-ref疊代時,您會得到相同的行為:
foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */
           

Here the important part is that foreach will make

$array

an is_ref=1 when it is iterated by reference, so basically you have the same situation as above.

在這裡重要的是,在通過引用進行疊代時,foreach将使

$array

成為is_ref = 1,是以基本上您具有與上述相同的情況。

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.

在開始循環時,這裡

$array

的引用計數為2,是以實際上一次必須做一次複制。

Thus

$array

and the array used by foreach will be completely separate from the outset.

是以,

$array

和foreach所使用的數組将從一開始就完全分開。

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

這就是為什麼要獲得IAP在循環之前的位置的原因(在這種情況下,它位于第一個位置)。

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.

嘗試在疊代過程中考慮修改是我們所有foreach問題的起源,是以可以考慮這種情況下的一些示例。

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

考慮在同一數組上的這些嵌套循環(使用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)
           

The expected part here is that

(1, 2)

is missing from the output because element

1

was removed.

此處的預期部分是因為元素

1

被删除,是以輸出中缺少

(1, 2)

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

HashPointer

.

其背後的原因是上述的嵌套循環hack:在循環主體運作之前,将目前IAP位置和哈希值備份到

HashPointer

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.

在循環體之後,它将被恢複,但是僅當元素仍然存在時才恢複,否則将使用目前IAP位置(無論可能是什麼)代替。

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!

在上面的示例中,情況确實如此:外循環的目前元素已被删除,是以它将使用IAP,該IAP已被内循環标記為完成!

Another consequence of the

HashPointer

backup+restore mechanism is that changes to the IAP through

reset()

etc. usually do not impact

foreach

.

HashPointer

備份+還原機制的另一個結果是,通過

reset()

等對IAP的更改通常不會影響

foreach

For example, the following code executes as if the

reset()

were not present at all:

例如,執行以下代碼,就像根本不存在

reset()

一樣:
$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

reset()

temporarily modifies the IAP, it will be restored to the current foreach element after the loop body.

原因是,盡管

reset()

臨時修改了IAP,但它将在循環體之後恢複為目前的foreach元素。

To force

reset()

to make an effect on the loop, you have to additionally remove the current element, so that the backup/restore mechanism fails:

要強制

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
           

But, those examples are still sane.

但是,這些例子仍然很理智。

The real fun starts if you remember that the

HashPointer

restore uses a pointer to the element and its hash to determine whether it still exists.

如果您還記得

HashPointer

還原使用指向該元素的指針及其哈希值以确定它是否仍然存在,那麼真正的樂趣就開始了。

But: Hashes have collisions, and pointers can be reused!

但是:哈希有沖突,并且指針可以重用!

This means that, with a careful choice of array keys, we can make

foreach

believe that an element that has been removed still exists, so it will jump directly to it.

這意味着,通過精心選擇數組鍵,我們可以讓

foreach

相信已删除的元素仍然存在,是以它将直接跳轉到該元素。

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

1, 1, 3, 4

according to the previous rules.

在這裡,我們應該通常期望的輸出

1, 1, 3, 4

,根據以往的規則。

How what happens is that

'FYFY'

has the same hash as the removed element

'EzFY'

, and the allocator happens to reuse the same memory location to store the element.

發生什麼情況是

'FYFY'

具有與删除的元素

'EzFY'

相同的哈希,并且配置設定器恰巧重用了相同的記憶體位置來存儲該元素。

So foreach ends up directly jumping to the newly inserted element, thus short-cutting the loop.

是以,foreach最終直接跳轉到新插入的元素,進而縮短了循環。

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.

我要提到的最後一個奇怪的情況是,PHP允許您在循環期間替換疊代的實體。

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就會從頭開始疊代另一個實體。

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

reset()

etc. on top of that.

為此,PHP 5使用了一個内部數組指針(IAP),這有些次優,因為必須擴充一個數組指針以支援多個同時的foreach循環以及與

reset()

等的互動。

PHP 7 uses a different approach, namely, it supports creating an arbitrary amount of external, safe hashtable iterators.

PHP 7使用了不同的方法,即,它支援建立任意數量的外部安全哈希表疊代器。

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.

這些疊代器必須在數組中注冊,從那時起,它們具有與IAP相同的語義:如果删除了數組元素,則指向該元素的所有哈希表疊代器都将前進到下一個元素。

This means that

foreach

will no longer use the IAP at all .

這意味着

foreach

将不再使用IAP 的 。

The

foreach

loop will be absolutely no effect on the results of

current()

etc. and its own behavior will never be influenced by functions like

reset()

etc.

foreach

循環絕對不會影響

current()

等的結果,并且它自己的行為永遠不會受到

reset()

等函數的影響。

Array duplication 陣列複制

Another important change between PHP 5 and PHP 7 relates to array duplication.

PHP 5和PHP 7之間的另一個重要變化涉及數組複制。

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.

現在,IAP不再使用,按值數組疊代将在所有情況下僅增加

refcount

(而不是複制數組)。

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.

如果在

foreach

循環中修改了數組,則将發生複制(根據寫時複制),并且

foreach

将繼續在舊數組上工作。

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.

在PHP 7中,這種特殊情況不複存在:數組的按值疊代将始終對原始元素進行處理,而無需考慮循環中的任何修改。

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 (ie 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. 測試用例1和2保留相同的輸出:按值數組疊代始終對原始元素起作用。 (In this case, even

    refcounting

    and duplication behavior is exactly the same between PHP 5 and PHP 7). (在這種情況下,甚至

    refcounting

    和複制行為在PHP 5和PHP 7之間也完全相同)。
  • Test case 3 changes:

    Foreach

    no longer uses the IAP, so

    each()

    is not affected by the loop. 測試用例3進行了更改:

    Foreach

    不再使用IAP,是以

    each()

    不受循環的影響。
    It will have the same output before and after. 前後會有相同的輸出。
  • Test cases 4 and 5 stay the same:

    each()

    and

    reset()

    will duplicate the array before changing the IAP, while

    foreach

    still uses the original array. 測試案例4和5保持不變:

    each()

    reset()

    将在更改IAP之前複制該數組,而

    foreach

    仍使用原始數組。
    (Not that the IAP change would have mattered, even if the array was shared.) (即使陣列是共享的,IAP更改也不會很重要。)

The second set of examples was related to the behavior of

current()

under different

reference/refcounting

configurations.

第二組示例與在不同

reference/refcounting

配置下

current()

的行為有關。

This no longer makes sense, as

current()

is completely unaffected by the loop, so its return value always stays the same.

這不再有意義,因為

current()

完全不受循環的影響,是以其傳回值始終保持不變。

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.

原因是兩個循環現在都具有完全獨立的哈希表疊代器,并且不再通過共享的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).

以前,HashPointer還原機制直接跳到新元素上,因為它“看起來”與删除的元素相同(由于哈希和指針沖突)。

As we no longer rely on the element hash for anything, this is no longer an issue.

由于我們不再依賴元素哈希進行任何操作,是以這不再是問題。

#5樓

NOTE FOR PHP 7

PHP 7的注意事項

To update on this answer as it has gained some popularity: This answer no longer applies as of PHP 7. As explained in the " Backward incompatible changes ", in PHP 7 foreach works on copy of the array, so any changes on the array itself are not reflected on foreach loop.

要更新該答案,因為它已經很流行:從PHP 7開始,此答案不再适用。如“ 向後不相容的更改 ”中所述,在PHP 7中,foreach可以在數組副本上使用,是以數組本身的任何更改沒有反映在foreach循環上。

More details at the link.

連結中有更多詳細資訊。

Explanation (quote from php.net ):

說明(引自php.net ):
The first form loops over the array given by array_expression. 第一種形式周遊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). 在每次疊代中,目前元素的值都配置設定給$ value,并且内部數組指針前進一個(是以,在下一次疊代中,您将檢視下一個元素)。

So, in your first example you only have one element in the array, and when the pointer is moved the next element does not exist, so after you add new element foreach ends because it already "decided" that it it as the last element.

是以,在第一個示例中,數組中隻有一個元素,并且當指針移動時,下一個元素不存在,是以在添加新元素後foreach結尾是因為它已經“決定”了它作為最後一個元素。

In your second example, you start with two elements, and foreach loop is not at the last element so it evaluates the array on the next iteration and thus realises that there is new element in the array.

在第二個示例中,您從兩個元素開始,并且foreach循環不在最後一個元素處,是以它在下一次疊代時對數組進行求值,進而意識到數組中存在新元素。

I believe that this is all consequence of On each iteration part of the explanation in the documentation, which probably means that

foreach

does all logic before it calls the code in

{}

.

我相信,這都是文檔中說明的“ 每次疊代”部分的全部結果,這可能意味着

foreach

在調用

{}

的代碼之前先進行所有邏輯。

Test case

測試用例

If you run this:

如果運作此指令:
<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        $array['baz']=3;
        echo $v." ";
    }
    print_r($array);
?>
           

You will get this output:

您将獲得以下輸出:
1 2 3 Array
(
    [foo] => 1
    [bar] => 2
    [baz] => 3
)
           

Which means that it accepted the modification and went through it because it was modified "in time".

這意味着它接受了修改并經曆了修改,因為修改是“及時的”。

But if you do this:

但是,如果您這樣做:
<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>
           

You will get:

你會得到:
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.

這意味着該數組已被修改,但是由于我們在

foreach

已位于數組的最後一個元素時對其進行了修改,是以它“決定”不再循環,即使我們添加了新元素,我們也将其添加為“太晚了”,它沒有循環通過。

Detailed explanation can be read at How does PHP 'foreach' actually work?

可以在PHP'foreach'實際如何工作中閱讀詳細說明。

which explains the internals behind this behaviour.

這解釋了此行為的内部原因。

#6樓

As per the documentation provided by PHP manual.

根據PHP手冊提供的文檔。
On each iteration, the value of the current element is assigned to $v and the internal 在每次疊代中,目前元素的值配置設定給$ v,内部 array pointer is advanced by one (so on the next iteration, you'll be looking at the next element). 數組指針前進一個(是以,在下一次疊代中,您将檢視下一個元素)。

So as per your first example:

是以,按照您的第一個示例:
$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
   $array['bar']=2;
   echo($v);
}
           

$array

have only single element, so as per the foreach execution, 1 assign to

$v

and it don't have any other element to move pointer

$array

僅具有單個元素,是以根據foreach執行,将1配置設定給

$v

,并且沒有其他元素可移動指針

But in your second example:

但是在第二個示例中:
$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
   $array['baz']=3;
   echo($v);
}
           

$array

have two element, so now $array evaluate the zero indices and move the pointer by one.

$array

有兩個元素,是以現在$ array計算零索引并将指針移一。

For first iteration of loop, added

$array['baz']=3;

對于循環的第一次疊代,添加了

$array['baz']=3;

as pass by reference.

作為參考。