foreach是PHP的關鍵字,用來實現基于數據的循環。基于數據循環語句的循環是由數據結構中的元素的數目來控制的。一般來說,基于數據的循環語句會使用一種稱之為迭代器的函數來實現元素的遍歷。
除了foreach,PHP還提供了預定義的一些函數來實現對數組的迭代訪問操作,如current, next, reset等等。然而我們使用得最多的還是foreach語句,foreach可以直接迭代訪問數組,如果用戶自己定義的對象需要使用此語句進行迭代訪問,必須實現SPL的迭代器。
這一小節,我們具體介紹PHP中foreach的實現過程。foreach 語法結構提供了遍歷數組的簡單方式。foreach 僅能夠應用于數組和對象,如果嘗試應用于其他數據類型的變量,或者未初始化的變量,將導致錯誤。foreach每次循環時,當前單元的值被賦給 $value 并且數組內部的指針向前移一步(因此下一次循環中將會得到下一個單元)。
### 循環過程的實現[]()
foreach語句在語法解析時對應三個操作:
1. zend_do_foreach_begin: 循環開始操作,生成FE_RESET中間代碼,數組會在循環開始時執行RESET操作,即我們使用foreach遍歷時不用每次重新手動RESET,同時此操作也會生成獲取變量的FE_FETCH中間代碼。
1. zend_do_foreach_cont:根據需要獲取變量的狀態判斷是否引用,此處的引用會影響FE_RESET的初始化操作和FE_FETCH中間代碼的獲取變量操作。
1. zend_do_foreach_end:設置ZEND_JMP中間代碼,設置下一條OP,以跳出循環,結束循環,清理工作。
這三個操作都是語法解析時對應的函數名,在編譯過程中會直接調用。他們形成的中間代碼在PHP內核執行時,形成的循環遍歷效果是:在foreach遍歷之前, PHP內核首先會有個FE_RESET操作來重置數組的內部指針,也就是pInternalPointer, 然后通過每次FE_FETCH將pInternalPointer指向數組的下一個元素,從而實現順序遍歷。并且每次FE_FETCH的結果都會被一個全局的中間變量存儲,以給下一次的獲取元素使用。
如下面這段代碼:
$arr = [array](http://www.php.net/array)(1, 2, 3, 4, 5);
?
foreach ($arr as $key => $row) {
[echo](http://www.php.net/echo) $key , $row;
}
這是一個標準的foreach循環使用示例。在VLD擴展中我們可以看到如下的中間代碼:
number of ops: 16
compiled vars: !0 = $arr, !1 = $key, !2 = $row
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > INIT_ARRAY ~0 1
1 ADD_ARRAY_ELEMENT ~0 2
2 ADD_ARRAY_ELEMENT ~0 3
3 ADD_ARRAY_ELEMENT ~0 4
4 ADD_ARRAY_ELEMENT ~0 5
5 ASSIGN !0, ~0
4 6 > FE_RESET $2 !0, ->14
7 > > FE_FETCH $3 $2, ->14
8 > ZEND_OP_DATA ~5
9 ASSIGN !2, $3
10 ASSIGN !1, ~5
5 11 ECHO !1
12 ECHO !2
6 13 > JMP ->7
14 > SWITCH_FREE $2
7 15 > RETURN 1
當我們通過RESET初始化數組后,FETCH會獲取變量,并將數組的內部指針指向一個元素。在前面我們講過,常規情況下OPCODE的執行是一條一條依次執行的,則在FE_FETCH獲取完變量后,PHP內核會依次執行后續的OPCODE,當執行到JMP時,會重新跳到->7,即再一次獲取變量,如此構成一個循環。當FE_FETCH執行失敗時,會跳轉到->14,即SWITCH_FREE,從而結束整個循環。
### 指針的意外行為[]()
在PHP手冊中有這樣一個NOTE:
> Note: 當 foreach 開始執行時,數組內部的指針會自動指向第一個單元。這意味著不需要在 foreach 循環之前調用 reset()。 由于 foreach 依賴內部數組指針,在循環中修改其值將可能導致意外的行為。
比如這樣一段代碼:
$arr = array(1,2,3,4,5);
?
foreach($arr as $key => $row) {
echo key($arr), '=>', current($arr), "\r\n";
}
如果在$row加上引用呢?如果在遍歷前添加 **$g = $arr;** 呢?結果是上面示例的代碼只會輸出數組的第二個元素。修改建議的代碼會依次輸出變量,但是第一個元素并沒有在輸出結果中出現。
這個異常引申出三個問題:
1.
為什么foreach循環體中執行key或current會顯示第二個元素(非引用情況)?以key函數為例,我們執行函數調用時,會執行中間代碼SEND_REF,此中間代碼會將沒有設置引用的變量復制一份并設置為引用。當進入循環體時,PHP內核已經經過了一次fetch操作,相當于執行了一次next操作,當前元素指向第二個元素。因此我們在foreach的循環體中執行key函數時,key中調用的數組變量為PHP執行了一次fetch操作的數組拷貝,此時foreach的內部指針指向第二個元素。
1.
為什么在foreach中執行end等操作,其循環過程不變?在遍歷的代碼中通過end,next等操作數組的指針,數組的指針不會變化,這是因為在PHP內核進行FETCH操作時,會通過中間變量存儲當前操作數組的內部指針,每遍歷一個元素,會先獲取之前存儲的指針位置,獲取下一個元素后,再恢復指針位置,關鍵在于FETCH OPCODE執行過程中的中間變量。
1.
為什么$row的引用和非引用情況下輸出結果不同?如果是引用,PHP內核在reset數組時,會直接分裂數組,生成一個數組的拷貝,并將其設置為引用。如果是非引用,PHP內核在reset數組時,當數組的引用計數大于1,并且不存在引用時,會拷貝數組供foreach使用,其它情況使用原數組,將其引用計數加1。因為引用的不同,在循環體中給函數傳遞參數時其結果不同,導致看到的foreach數組內部指針變化的不同。對于非引用且引用計數大于1的情況,其本身就是兩個不同的數組,在RESET時就不同了。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊