PHP 內核:foreach 是如何工作的(一)

foreach 是如何工作的?

PHP 內核:foreach 是如何工作的(二)

首先聲明,我知道 foreach 是什麼,也知道怎麼去用它。但這個問題關心的是,內核中 foreach 是如何運行的,我不想回答關於 “如何使用 foreach 循環數組” 的任何問題。

很長時間我都認爲 foreach 是直接作用於數組本身,後來一些資料表明,它作用於數組的一個副本,那時我以爲這就是真相了。但最近我又討論了一下這件事,經過一些試驗,發現我之前的想法並非完全正確。

 

讓我來展示一下我的觀點。下面的測試用例中我們將使用以下數組:

 

$array = array(1, 2, 3, 4, 5);

測試用例 1:

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

這很清晰的表明我們不直接使用數據源 - 否則循環會一直持續下去,因此我們可以在循環中不停的推送元素到數組中。爲了保證正確請看下面的測試用例:

測試用例 2:

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 依賴源數組的指針。但是我們剛剛證明我們 沒有使用源數組,對吧?好吧,不完全是。

測試用例 3:

// 將數組指針移動到一個上面確保它不會影響循環
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 依賴與內部數組指針,因此在循環內部改變它可能導致意外的行爲。

讓我們找出那種 “意外行爲” 是什麼 (從技術上講,任何行爲都是意外的,因爲我們也不知道將會發生什麼)。

測試用例 4:

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* 輸出: 1 2 3 4 5 */

測試用例 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 對象,因爲這些 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等多個知識點高級進階乾貨需要的可以免費分享給大家,需要的可以加入我的官方羣點擊此處

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章