foreach 是如何工作的?
首先聲明,我知道 foreach 是什麼,也知道怎麼去用它。但這個問題關心的是,內核中 foreach 是如何運行的,我不想回答關於 “如何使用 foreach 循環數組” 的任何問題。
很長時間我都認爲 foreach 是直接作用於數組本身,後來一些資料表明,它作用於數組的一個副本,那時我以爲這就是真相了。但最近我又討論了一下這件事,經過一些試驗,發現我之前的想法並非完全正確。
讓我來展示一下我的觀點。下面的測試用例中我們將使用以下數組:
$array = array(1, 2, 3, 4, 5);
foreach ($array as $item) {
echo "$item\n";
$array[] = $item;
}
print_r($array);
/* 循環中輸出: 1 2 3 4 5
循環後的$array: 1 2 3 4 5 1 2 3 4 5 */
這很清晰的表明我們不直接使用數據源 - 否則循環會一直持續下去,因此我們可以在循環中不停的推送元素到數組中。爲了保證正確請看下面的測試用例:
foreach ($array as $key => $item) {
$array[$key + 1] = $item + 2;
echo "$item\n";
}
print_r($array);
/* 循環中輸出: 1 2 3 4 5
循環後 $array: 1 3 4 5 6 7 */
這印證了我們的初步結論,在循環中使用的是數組的副本,否則我們將看到在循環中改變後的值。 但是...
如果我們查閱手冊 手冊,我們會發現下面這句話:
當 foreach 首次開始執行時,數組的內部指針自動重置爲數組的第一個元素。
沒錯。。。這似乎表明 foreach 依賴源數組的指針。但是我們剛剛證明我們 沒有使用源數組,對吧?好吧,不完全是。
// 將數組指針移動到一個上面確保它不會影響循環
var_dump(each($array));
foreach ($array as $item) {
echo "$item\n";
}
var_dump(each($array));
/* 輸出
array(4) {
[1]=>
int(1)
["value"]=>
int(1)
[0]=>
int(0)
["key"]=>
int(0)
}
1
2
3
4
5
bool(false)
*/
因此,儘管我們不支持使用源數組,但是直接使用源數組指針 - 指針位於循環結束時的數組末尾證明了這一點。除非這不是真的 - 如果是,那麼 測試用例 1 將永遠循環。
PHP 手冊還說明:
由於 foreach 依賴與內部數組指針,因此在循環內部改變它可能導致意外的行爲。
讓我們找出那種 “意外行爲” 是什麼 (從技術上講,任何行爲都是意外的,因爲我們也不知道將會發生什麼)。
foreach ($array as $key => $item) {
echo "$item\n";
each($array);
}
/* 輸出: 1 2 3 4 5 */
foreach ($array as $key => $item) {
echo "$item\n";
reset($array);
}
/* 輸出: 1 2 3 4 5 */
... 意料之中,事實上它似乎支持 「複製源」 理論。
問題
這是怎麼回事呢?我的 C-fu 還不夠好,不能通過簡單地查看 PHP 源代碼就得出正確的結論,如果有人能把它翻譯成英語,我將不勝感激。
在我看來,foreach
使用數組的 copy ,但是在循環之後將源數組的數組指針設置爲數組的末尾。
- 這是真的嗎?
- 如果不是,正確的流程是什麼樣的呢?
- 在
foreach
期間使用調整數組指針 (each()
,reset()
等) 的函數是否會影響循環的結果呢?
解答
foreach
支持三種不同類型值的迭代:
- 數組
- 普通對象
Traversable
對象
在下面的討論中,我將嘗試準確的解釋迭代在不同的場景中是如何工作的。到目前爲止,最簡單的例子是 Traversable
對象,因爲這些 foreach
本質上只是以下代碼的語法糖:
foreach ($it as $k => $v) { /* ... */ }
/* 轉換爲: */
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 中,“數組” 實際上是有序的字典,它們將按照這個順序遍歷(只要不使用形如 sort 一類的函數對其排序,它就能按照插入的順序遍歷)。這與按照鍵的自然順序迭代(其他語言中的列表通常是如何排序的呢?)或者無序(其他語言中的字典通常是如何工作的呢)是截然不同的。
同樣的情況也適用於對象,因爲對象屬性可以看做是另一個(有序的)字典,將屬性名映射爲對應的值,並加上一些可見性的操作處理。在大多數情況下,對象屬性實際上並不是以這種低效的方式存儲的。然而,如果你開始遍歷一個對象,通常它將被打包轉換爲一個真正的字典。在這一點上,普通對象的迭代與數組的迭代非常相似(這就是爲什麼我在這沒有過多討論普通對象的迭代)。
到目前爲止,一切順利。遍歷字典應該不難,對吧?當你意識到數組 / 對象可以在迭代期間更改時,問題就出現了。發生這種情況有如下幾種:
- 如果你使用 foreach($arr as &$v) 通過引用迭代,那麼 $arr 將會轉爲引用,你可以在迭代期間更改它。
- 在 PHP 5 中,即使按值迭代也是如此,但數組之前是一個引用:$ref=&$arr;foreach($ref as $v)。
- 對象具有處理傳遞語義的功能,對於大多數實際用途而言,它們的行爲類似於引用。 因此,在迭代期間總是可以更改對象。
在迭代期間允許修改的問題是刪除當前所在元素的情況。假設你使用指針來跟蹤你當前所在的數組元素。 如果現在釋放了此元素,則會留下懸空指針(通常會導致 segfault 段錯誤)
有不同的方法來解決這個問題。 PHP 5 和 PHP 7 在這方面有很大不同,我將在下面描述這兩種情況。 總結是 PHP 5 的方法相當愚蠢並導致出現各種奇怪的邊緣情況問題,而 PHP 7 更復雜的方法導致出現更可預測和一致的行爲情況。
初步得出結論,PHP 是使用引用計數和寫時複製來管理內存。 這意味着如果你 “複製” 一個值,實際上只是複用其舊值並增加其引用計數(refcount)。 只有在執行某種修改後,纔會執行其真正的副本(複製)。 請參閱 你被騙了,以獲得有關此主題的更多的介紹。
更多學習內容請訪問:
騰訊T3-T4標準精品PHP架構師教程目錄大全,只要你看完保證薪資上升一個臺階(持續更新)
以上內容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提升,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨需要的可以免費分享給大家,需要的可以加入我的官方羣點擊此處。