php用法 - php foreach key




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保持不變:在更改IAP之前, each()reset()將復制數組,而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);
?>

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.

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