foreach判断最后一个_PHP 内核:foreach 是如何工作的(二)
PHP 5
內(nèi)部數(shù)組指針和散列指針
PHP 5 中的數(shù)組有一個專用的 “內(nèi)部數(shù)組指針”(IAP),它適當(dāng)?shù)刂С中薷?#xff1a;每當(dāng)刪除一個元素時,都會檢查 IAP 是否指向該元素。 如果是,則轉(zhuǎn)發(fā)到下一個元素。
雖然 foreach 確實使用了 IAP,但還有一個復(fù)雜因素:只有一個 IAP,但是一個數(shù)組可以是多個 foreach 循環(huán)的一部分:
// 在這里使用by-ref迭代來確保它真的 // 兩個循環(huán)中的相同數(shù)組而不是副本 foreach ($arr as &$v1) {foreach ($arr as &$v) {// ...} }為了支持只有一個內(nèi)部數(shù)組指針的兩個同時循環(huán),foreach 執(zhí)行以下 shenanigans:在執(zhí)行循環(huán)體之前,foreach 將備份指向當(dāng)前元素及其散列的指針到每個 foreachHashPointer。循環(huán)體運行后,如果 IAP 仍然存在,IAP 將被設(shè)置回該元素。 但是,如果元素已被刪除,我們將只在 IAP 當(dāng)前所在的位置使用。這個計劃基本上是可行的,但是你可以從中獲得很多奇怪的情況,其中一些我將在下面演示。
數(shù)組復(fù)制
IAP 是數(shù)組的一個可見特性 (通過 current 系列函數(shù)公開),因此 IAP 計數(shù)的更改是在寫時復(fù)制語義下的修改。不幸的是,這意味著 foreach 在許多情況下被迫復(fù)制它正在迭代的數(shù)組。 具體條件是:
如果數(shù)組沒有被復(fù)制 (is_ref=0, refcount=1),那么只有它的 refcount 會被增加 (*)。此外,如果使用帶引用的 foreach,那么 (可能重復(fù)的) 數(shù)組將轉(zhuǎn)換為引用。
如下代碼作為引起復(fù)制的示例:
function iterate($arr) {foreach ($arr as $v) {} }$outerArr = [0, 1, 2, 3, 4]; iterate($outerArr);在這里,$arr 將被復(fù)制以防止 $arr 上的 IAP 更改泄漏到 $outerArr。 就上述條件而言,數(shù)組不是引用(is_ref = 0),并且在兩個地方使用(refcount = 2)。 這個要求是不幸的,也是次優(yōu)實現(xiàn)的工件(這里不需要修改迭代,因此我們不需要首先使用 IAP)。
(*)增加 refcount 聽起來無害,但違反了寫時復(fù)制(COW)語義:這意味著我們要修改 refcount = 2 數(shù)組的 IAP,而 COW 則要求只能執(zhí)行修改 on refcount = 1 值。這種違反會導(dǎo)致用戶可見的行為更改 (而 COW 通常是透明的),因為迭代數(shù)組上的 IAP 更改將是可見的 -- 但只有在數(shù)組上的第一個非 IAP 修改之前。相反,這三個 “有效” 選項是:a) 始終復(fù)制,b) 不增加 refcount,從而允許在循環(huán)中任意修改迭代數(shù)組,c) 完全不使用 IAP (PHP 7 解決方案)。
位置發(fā)展順序
要正確理解下面的代碼示例,你必須了解最后一個實現(xiàn)細(xì)節(jié)。在偽代碼中,循環(huán)遍歷某些數(shù)據(jù)結(jié)構(gòu)的 “正常” 方法是這樣的:
reset(arr); while (get_current_data(arr, &data) == SUCCESS) {code();move_forward(arr); }然而,foreach,作為一個相當(dāng)特殊的 snowflake,選擇做的事情略有不同:
reset(arr); while (get_current_data(arr, &data) == SUCCESS) {move_forward(arr);code(); }也就是說,數(shù)組指針 在循環(huán)體運行之前已經(jīng)向前移動了。這意味著,當(dāng)循環(huán)體處理元素 $i 時,IAP 已經(jīng)位于元素 $i+1。這就是為什么在迭代期間顯示修改的代碼示例總是 unset下一個元素,而不是當(dāng)前元素的原因。
例子:你的測試用例
上面描述的三個方面應(yīng)該可以讓你大致了解 foreach 實現(xiàn)的特性,我們可以繼續(xù)討論一些例子。
此時,測試用例的行為更容易理解:
- 在測試用例 1 和 2 中,$array 以 refcount = 1 開始,因此它不會被 foreach 復(fù)制:只有 refcount 才會遞增。 當(dāng)循環(huán)體隨后修改數(shù)組(在該點處具有 refcount = 2)時,將在該點處進(jìn)行復(fù)制。 Foreach 將繼續(xù)處理未修改的 $array 副本。
- 在測試用例 3 中,數(shù)組沒有再被復(fù)制,因此 foreach 將修改 $array 變量的 IAP。 在迭代結(jié)束時,IAP 為 NULL(意味著迭代已完成),其中 each 返回 false。
- 在測試用例 4 和 5 中,each 和 reset 都是引用函數(shù)。$array 在傳遞給它們時有一個 refcount = 2,所以必須復(fù)制它。因此,foreach 將再次處理一個單獨的數(shù)組。
例子:current 在 foreach 中的作用
顯示各種復(fù)制行為的一個好方法是觀察 foreach 循環(huán)中 current() 函數(shù)的行為。看如下這個例子:
foreach ($array as $val) {var_dump(current($array)); } /* 輸出: 2 2 2 2 2 */在這里,你應(yīng)該知道 current() 是一個 by-ref 函數(shù) (實際上是:preferences-ref),即使它沒有修改數(shù)組。它必須很好地處理所有其他函數(shù),如 next,它們都是 by-ref。通過引用傳遞意味著數(shù)組必須是分開的,因此 $array 和 foreach-array 將是不同的。你得到是 2 而不是 1 的原因也在上面提到過:foreach在運行用戶代碼之前指向數(shù)組指針,而不是之后。因此,即使代碼位于第一個元素,foreach 已經(jīng)將指針指向第二個元素。
現(xiàn)在讓我們嘗試一下小修改:
$ref = &$array; foreach ($array as $val) {var_dump(current($array)); } /* 輸出: 2 3 4 5 false */這里我們有 is_ref=1 的情況,所以數(shù)組沒有被復(fù)制 (就像上面那樣)。但是現(xiàn)在它是一個引用,當(dāng)傳遞給 by-ref current() 函數(shù)時不再需要復(fù)制數(shù)組。因此,current() 和 foreach 工作在同一個數(shù)組上。不過,由于 foreach 指向指針的方式,你仍可以看到 off-by-one 行為。
當(dāng)執(zhí)行 by-ref 迭代時,你會得到相同的行為:
foreach ($array as &$val) {var_dump(current($array)); } /* 輸出: 2 3 4 5 false */這里重要的部分是,當(dāng)通過引用迭代 $array 時,foreach 會將 $array 設(shè)置為 is_ref=1,所以基本上情況與上面相同。
另一個小變化,這次我們將數(shù)組分配給另一個變量:
$foo = $array; foreach ($array as $val) {var_dump(current($array)); } /* 輸出: 1 1 1 1 1 */這里 $array 的 refcount 在循環(huán)開始時是 2,所以這一次我們必須在前面進(jìn)行復(fù)制。因此,$array 和 foreach 使用的數(shù)組從一開始就完全分離。這就是為什么 IAP 的位置在循環(huán)之前的任何位置 (在本例中是在第一個位置)。
例子:迭代期間的修改
嘗試?yán)斫獾^程中的修改是我們所有 foreach 問題的起源,因此我們可以拿一些例子來考慮。
考慮相同數(shù)組上的這些嵌套循環(huán) (其中 by-ref 迭代用于確保它確實是相同的):
foreach ($array as &$v1) {foreach ($array as &$v2) {if ($v1 == 1 && $v2 == 1) {unset($array[1]);}echo "($v1, $v2)n";} }// 輸出: (1, 1) (1, 3) (1, 4) (1, 5)這里的預(yù)期部分是輸出中缺少 (1,2),因為元素 1 被刪除了。可能出乎意料的是,外部循環(huán)在第一個元素之后停止。這是為什么呢?
這背后的原因是上面描述的嵌套循環(huán)攻擊:在循環(huán)體運行之前,當(dāng)前 IAP 位置和散列被備份到一個 HashPointer 中。在循環(huán)體之后,它將被恢復(fù),但是只有當(dāng)元素仍然存在時,否則將使用當(dāng)前 IAP 位置 (無論它是什么)。在上面的例子中,情況正是這樣:外部循環(huán)的當(dāng)前元素已經(jīng)被刪除,所以它將使用 IAP,而內(nèi)部循環(huán)已經(jīng)將 IAP 標(biāo)記為 finished !
HashPointer 備份 + 恢復(fù)機(jī)制的另一個結(jié)果是,通過 reset() 等方法更改 IAP。通常不會影響 foreach。例如,下面的代碼執(zhí)行起來就像根本不存在 reset() 一樣:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
var_dump($value);
reset($array);
}
// 輸出: 1, 2, 3, 4, 5
原因是,當(dāng) reset() 暫時修改 IAP 時,它將恢復(fù)到循環(huán)體后面的當(dāng)前 foreach 元素。要強(qiáng)制 reset() 對循環(huán)產(chǎn)生影響,你必須刪除當(dāng)前元素,這樣備份 / 恢復(fù)機(jī)制就會失敗:
$array = [1, 2, 3, 4, 5]; $ref =& $array; foreach ($array as $value) {var_dump($value);unset($array[1]);reset($array); } // 輸出: 1, 1, 3, 4, 5但是,這些例子仍是合理的。如果你還記得 HashPointer 還原使用指向元素及其散列的指針來確定它是否仍然存在,那么真正的樂趣就開始了。但是:散列有沖突,指針可以重用!這意味著,通過仔細(xì)選擇數(shù)組鍵,我們可以讓 foreach 相信被刪除的元素仍然存在,因此它將直接跳轉(zhuǎn)到它。一個例子:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3]; $ref =& $array; foreach ($array as $value) {unset($array['EzFY']);$array['FYFY'] = 4;reset($array);var_dump($value); } // 輸出: 1, 4這里根據(jù)前面的規(guī)則,我們通常期望輸出 1,1,3,4。實際情況上'FYFY' 具有與刪除的元素'FYFY' 相同的散列,而分配器恰好重用相同的內(nèi)存位置來存儲元素。因此,foreach 最終直接跳轉(zhuǎn)到新插入的元素,從而縮短了循環(huán)。
在循環(huán)期間替換迭代實體
我想提到的最后一個奇怪的情況是,PHP 允許你在循環(huán)期間替換迭代實體。所以你可以開始在一個數(shù)組上迭代然后在中間用另一個數(shù)組替換。或者用一個對象來替換:
$arr = [1, 2, 3, 4, 5]; $obj = (object) [6, 7, 8, 9, 10];$ref =& $arr; foreach ($ref as $val) {echo "$valn";if ($val == 3) {$ref = $obj;} } /* 輸出: 1 2 3 6 7 8 9 10 */正如你在本例中所看到的,一旦替換發(fā)生,PHP 將從頭開始迭代另一個實體。
PHP 7
散列表迭代器
如果你還記得,數(shù)組迭代的主要問題是如何處理迭代過程中元素的刪除。PHP 5 為此使用了一個內(nèi)部數(shù)組指針 (IAP),這有點不太理想,因為一個數(shù)組指針必須被拉伸以支持多個同時進(jìn)行的 foreach 循環(huán)和與 reset() 等的交互。最重要的是。
PHP 7 使用了一種不同的方法,即支持創(chuàng)建任意數(shù)量的外部安全散列表迭代器。這些迭代器必須在數(shù)組中注冊,從這一點開始,它們具有與 IAP 相同的語義:如果刪除了一個數(shù)組元素,那么指向該元素的所有 hashtable 迭代器都將被提升到下一個元素。
這意味著 foreach 將不再使用 IAP。foreach 循環(huán)絕對不會影響 current() 等的結(jié)果。它自己的行為永遠(yuǎn)不會受到像 reset() 等函數(shù)的影響。
數(shù)組復(fù)制
PHP 5 和 PHP 7 之間的另一個重要更改與數(shù)組復(fù)制有關(guān)。現(xiàn)在 IAP 不再使用了,在所有情況下,按值數(shù)組迭代將只執(zhí)行 refcount 增量 (而不是復(fù)制數(shù)組)。如果數(shù)組在 foreach 循環(huán)期間被修改,那么此時將發(fā)生復(fù)制 (根據(jù)寫時復(fù)制),而 foreach 將繼續(xù)處理舊數(shù)組。
在大多數(shù)情況下,這種更改是透明的,除了更好的性能之外沒有其他效果。但是,有一種情況會導(dǎo)致不同的行為,即數(shù)組前是一個引用:
$array = [1, 2, 3, 4, 5]; $ref = &$array; foreach ($array as $val) {var_dump($val);$array[2] = 0; } /* 舊輸出: 1, 2, 0, 4, 5 */ /* 新輸出: 1, 2, 3, 4, 5 */以前,引用數(shù)組的按值迭代是一種特殊情況。在本例中,沒有發(fā)生重復(fù),因此在迭代期間對數(shù)組的所有修改都將由循環(huán)反映出來。在 PHP 7 中,這種特殊情況消失了:數(shù)組的按值迭代將始終繼續(xù)處理原始元素,而不考慮循環(huán)期間的任何修改。
當(dāng)然,這不適用于 by-reference 迭代。如果你通過引用進(jìn)行迭代,那么所有的修改都將被循環(huán)所反映。有趣的是,對于普通對象的按值迭代也是如此:
$obj = new stdClass; $obj->foo = 1; $obj->bar = 2; foreach ($obj as $val) {var_dump($val);$obj->bar = 42; } /* 新舊輸出: 1, 42 */這反映了對象的按句柄語義 (即,即使在按值上下文中,它們的行為也類似于引用)。
例子
讓我們考慮幾個例子,從你的測試用例開始:
測試用例 1 和 2 輸出相同:按值數(shù)組迭代始終在原始元素上工作。(在本例中,甚至 refcounting 和復(fù)制行為在 PHP 5 和 PHP 7 之間也是完全相同的)。
測試用例 3 的變化:Foreach 不再使用 IAP,因此 each() 不受循環(huán)影響。前后輸出一樣。
測試用例 4 和 5 保持不變:each() 和 reset() 將在更改 IAP 之前復(fù)制數(shù)組,而 foreach 仍然使用原始數(shù)組。(即使數(shù)組是共享的,IAP 的更改也無關(guān)緊要。)
第二組示例與 current() 在不同 reference/refcounting 配置下的行為有關(guān)。這不再有意義,因為 current() 完全不受循環(huán)影響,所以它的返回值總是保持不變。
然而,當(dāng)考慮迭代過程中的修改時,我們得到了一些有趣的變化。我希望你會發(fā)現(xiàn)新的行為更加健全。 第一個例子:
$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";} }// 舊輸出: (1, 1) (1, 3) (1, 4) (1, 5) // 新輸出: (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)如你所見,外部循環(huán)在第一次迭代之后不再中止。原因是現(xiàn)在兩個循環(huán)都有完全獨立的 hashtable 散列表迭代器,并且不再通過共享的 IAP 對兩個循環(huán)進(jìn)行交叉污染。
現(xiàn)在修復(fù)的另外一個奇怪的邊緣現(xiàn)象是,當(dāng)刪除并且添加恰好具有相同的哈希元素時,會得到奇怪的結(jié)果:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3]; foreach ($array as &$value) {unset($array['EzFY']);$array['FYFY'] = 4;var_dump($value); } // 舊輸出: 1, 4 // 新輸出: 1, 3, 4之前的 HashPointer 恢復(fù)機(jī)制直接跳轉(zhuǎn)到新元素,因為它 “看起來” 和刪除的元素相同(由于哈希和指針沖突)。由于我們不再依賴于哈希元素,因此不再是一個問題。
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的foreach判断最后一个_PHP 内核:foreach 是如何工作的(二)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: combobox 怎么实现对listvi
- 下一篇: git拉取tag_不给队友拖后腿!团队开