深入解析PHP中foreach語句控制數組循環
深入解析PHP中foreach語句控制數組循環的用法,是PHP入門學習中的基礎知識,需要的朋友可以參考下。
foreach是PHP中很常用的一個用作數組循環的控制語句
因為它的方便和易用,自然也就在後端隱藏着很複雜的具體實現方式(對用户透明)
今天,我們就來一起分析分析,foreach是如何實現數組(對象)的遍歷的。
我們知道PHP是一個腳本語言,也就是説,用户編寫的PHP代碼最終都是會被PHP解釋器解釋執行,
特別的,對於PHP來説,所有的用户編寫的PHP代碼,都會被翻譯成PHP的虛擬機ZE的虛擬指令(OPCODES)來執行,不論細節的話,就是説,我們所編寫的任何PHP腳本,都會最終被翻譯成一條條的指令,從而根據指令,由相應的C編寫的函數來執行。
那麼foreach會被翻譯成什麼樣子呢?
foreach($arr as $key => $vAL){
echo $key . '=>' . $val . " ";
}
在詞法分析階段,foreach會被識別為一個TOKEN:T_FOREACH,
在語法分析階段,會被規則:
unticked_statement: //沒有被綁定ticks的語句
//有省略
| T_FOREACH '(' variable T_AS
{ zend_do_foreach_begin(&$1, &$2, &$3, &$4, 1 TSRMLS_CC); }
foreach_variable foreach_optional_arg ')' { zend_do_foreach_cont(&$1, &$2, &$4, &$6, &$7 TSRMLS_CC); }
foreach_statement { zend_do_foreach_end(&$1, &$4 TSRMLS_CC); }
| T_FOREACH '(' expr_without_variable T_AS
{ zend_do_foreach_begin(&$1, &$2, &$3, &$4, 0 TSRMLS_CC); }
variable foreach_optional_arg ')' { zend_check_writable_variable(&$6); zend_do_foreach_cont(&$1, &$2, &$4, &$6, &$7 TSRMLS_CC); }
foreach_statement { zend_do_foreach_end(&$1, &$4 TSRMLS_CC); }
//有省略
;
仔細分析這段語法規則,我們可以發現,對於:
foreach($arr as $key => $val){
echo $key . ‘=>' . $val .” ”;
}
會被分析為:
T_FOREACH '(' variable T_AS { zend_do_foreach_begin('foreach', '(', $arr, 'as', 1 TSRMLS_CC); }
foreach_variable foreach_optional_arg(T_DOUBLE_ARROW foreach_variable) ')' { zend_do_foreach_cont('foreach', '(', 'as', $key, $val TSRMLS_CC); }
foreach_satement {zend_do_foreach_end('foreach', 'as');}
然後,讓我們來看看foreach_statement:
它其實就是一個代碼塊,體現了我們的 echo $key . ‘=>' . $val .” ”;
T_ECHO expr;
顯然,實現foreach的核心就是如下3個函數:
zend_do_foreach_begin
zend_do_foreach_cont
zend_do_foreach_end
其中,zend_do_foreach_begin (代碼太長,直接寫偽碼) 主要做了:
1. 記錄當前的opline行數(為以後跳轉而記錄)
2. 對數組進行RESET(講內部指針指向第一個元素)
3. 獲取臨時變量 ($val)
4. 設置獲取變量的OPCODE FE_FETCH,結果存第3步的臨時變量
4. 記錄獲取變量的OPCODES的行數
而對於 zend_do_foreach_cont來説:
1. 根據foreach_variable的來判斷是否引用
2. 根據是否引用來調整zend_do_foreach_begin中生成的FE_FETCH方式
3. 根據zend_do_foreach_begin中記錄的取變量的OPCODES的行數,來初始化循環(主要處理在循環內部的循環:do_begin_loop)
最後zend_do_foreach_end:
1. 根據zend_do_foreach_begin中記錄的行數信息,設置ZEND_JMP OPCODES
2. 根據當前行數,設置循環體下一條opline, 用以跳出循環
3. 結束循環(處理循環內循環:do_end_loop)
4. 清理臨時變量
當然, 在zend_do_foreach_cont 和 zend_do_foreach_end之間 會在語法分析階段被填充foreach_satement的語句代碼。
這樣,就實現了foreach的OPCODES line。
比如對於我們開頭的實例代碼,最終生成的OPCODES是:
filename: /home/huixinchen/
function name: (null)
number of ops: 17
compiled vars: !0 = $arr, !1 = $key, !2 = $val
line # op fetch ext return operands
-------------------------------------------------------------------------------
2 0 SEND_VAL 1
1 SEND_VAL 100
2 DO_FCALL 2 'range'
3 ASSIGN !0, $0
3 4 FE_RESET $2 !0, ->14
5 FE_FETCH $3 $2, ->14
6 ZEND_OP_DATA ~5
7 ASSIGN !2, $3
8 ASSIGN !1, ~5
4 9 CONCAT ~7 !1, '-'
10 CONCAT ~8 ~7, !2
11 CONCAT ~9 ~8, '%0A'
12 ECHO ~9
5 13 JMP ->5
14 SWITCH_FREE $2
7 15 RETURN 1
16* ZEND_HANDLE_EXCEPTION
我們注意到FE_FETCH的op2的操作數是14,也就是JMP後一條opline,也就是説,在獲取完最後一個數組元素以後,FE_FETCH失敗的情況下,會跳到第14行opline,從而實現了循環的結束。
而15行opline的op1的操作數是指向了FE_FETCH,也就是無條件跳轉到第5行opline,從而實現了循環。
附錄:
void zend_do_foreach_begin(znode *foreach_token, znode *open_brackets_token, znode *array, znode *as_token, int variable TSRMLS_DC)
{
zend_op *opline;
zend_bool is_variable;
zend_bool push_container = 0;
zend_op dummy_opline;
if (variable) {
//是否是匿名數組
if (zend_is_function_or_method_call(array)) {
//是否是函數返回值
is_variable = 0;
} else {
is_variable = 1;
}
/* 使用括號記錄FE_RESET的opline行數 */
open_brackets_token->ne_num = get_next_op_number(CG(active_op_array));
zend_do_end_variable_parse(BP_VAR_W, 0 TSRMLS_CC); //獲取數組/對象和zend_do_begin_variable_parse對應
if (CG(active_op_array)->last > 0 &&
CG(active_op_array)->opcodes[CG(active_op_array)->last-1]de == ZEND_FETCH_OBJ_W) {
/* Only lock the container if we are fetching from a real container and not $this */
if (CG(active_op_array)->opcodes[CG(active_op_array)->last-1]_type == IS_VAR) {
CG(active_op_array)->opcodes[CG(active_op_array)->last-1]nded_value |= ZEND_FETCH_ADD_LOCK;
push_container = 1;
}
}
} else {
is_variable = 0;