## 4.2 選擇結構
程序并不都是順序執行的,選擇結構用于判斷給定的條件,根據判斷的結果來控制程序的流程。PHP中通過if、elseif、else和switch語句實現條件控制。這一節我們就分析下PHP中兩種條件語句的具體實現。
### 4.2.1 if語句
If語句用法:
```php
if(Condition1){
Statement1;
}elseif(Condition2){
Statement2;
}else{
Statement3;
}
```
IF語句有兩部分組成:condition(條件)、statement(聲明),每個條件分支對應一組這樣的組合,其中最后的else比較特殊,它沒有條件,編譯時也是按照這個邏輯編譯為一組組的condition和statement,其具體的語法規則如下:
```c
if_stmt:
if_stmt_without_else %prec T_NOELSE { $$ = $1; }
| if_stmt_without_else T_ELSE statement
{ $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_IF_ELEM, NULL, $3)); }
;
if_stmt_without_else:
T_IF '(' expr ')' statement { $$ = zend_ast_create_list(1, ZEND_AST_IF,
zend_ast_create(ZEND_AST_IF_ELEM, $3, $5)); }
| if_stmt_without_else T_ELSEIF '(' expr ')' statement
{ $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_IF_ELEM, $4, $6)); }
;
```
從上面的語法規則可以看出,編譯if語句時首先會創建一個`ZEND_AST_IF`的節點,這個節點是一個list,用于保存各個分支的condition、statement,編譯每個分支時將創建一個`ZEND_AST_IF_ELEM`的節點,它有兩個子節點,分別用來記錄:condition、statement,然后把這個節點插入到`ZEND_AST_IF`下,最終生成的AST:

編譯opcode時順序編譯每個分支的condition、statement即可,編譯過程大致如下:
* __(1)__ 編譯當前分支的condition語句,這里可能會有多個條件,但最終會歸并為一個true/false的結果;
* __(2)__ 編譯完condition后編譯一條ZEND_JMPZ的opcode,這條opcode用來判斷當前condition最終為true還是false,如果當前condition成立直接繼續執行本組statement即可,無需進行跳轉,但是如果不成立就需要跳過本組的statement,所以這條opcode還需要知道該往下跳過多少條opcode,而跳過的這些opcode就是本組的statement,因此這個值需要在編譯完本組statement后才能確定,現在還無法確定;
* __(3)__ 編譯當前分支的statement列表,其節點類型ZEND_AST_STMT_LIST,就是普通語句的編譯;
* __(4)__ 編譯完statement后編譯一條ZEND_JMP的opcode,這條opcode是當condition成立執行完本組statement時跳出if的,因為當前分支既然條件成立就不需要再跳到其他分支,執行完當前分支的statement后將直接跳出if,所以ZEND_JMP需要知道該往下跳過多少opcode,而跳過的這些opcode是后面所有分支的opcode數,只有編譯完全部分支后才能確定;
* __(5)__ 編譯完statement后再設置步驟(2)中條件不成立時ZEND_JMPZ應該跳過的opcode數;
* __(6)__ 重復上面的過程依次編譯后面的condition、statement,編譯完全部分支后再設置各分支在步驟(4)中ZEND_JMP跳出if的opcode位置。
具體的編譯過程在`zend_compile_if()`中,過程比較清晰:
```c
void zend_compile_if(zend_ast *ast)
{
zend_ast_list *list = zend_ast_get_list(ast);
uint32_t i;
uint32_t *jmp_opnums = NULL;
//用來保存每個分支在步驟(4)中的ZEND_JMP opcode
if (list->children > 1) {
jmp_opnums = safe_emalloc(sizeof(uint32_t), list->children - 1, 0);
}
//依次編譯各個分支
for (i = 0; i < list->children; ++i) {
zend_ast *elem_ast = list->child[i];
zend_ast *cond_ast = elem_ast->child[0]; //條件
zend_ast *stmt_ast = elem_ast->child[1]; //聲明
znode cond_node;
uint32_t opnum_jmpz;
if (cond_ast) {
//編譯condition
zend_compile_expr(&cond_node, cond_ast);
//編譯condition跳轉opcode:ZEND_JMPZ
opnum_jmpz = zend_emit_cond_jump(ZEND_JMPZ, &cond_node, 0);
}
//編譯statement
zend_compile_stmt(stmt_ast);
//編譯statement執行完后跳出if的opcode:ZEND_JMP(最后一個分支無需這條opcode)
if (i != list->children - 1) {
jmp_opnums[i] = zend_emit_jump(0);
}
if (cond_ast) {
//設置ZEND_JMPZ跳過opcode數
zend_update_jump_target_to_next(opnum_jmpz);
}
}
if (list->children > 1) {
//設置前面各分支statement執行完后應跳轉的位置
for (i = 0; i < list->children - 1; ++i) {
zend_update_jump_target_to_next(jmp_opnums[i]); //設置每組stmt最后一條jmp跳轉為if外
}
efree(jmp_opnums);
}
}
```
最終if語句編譯后基本是這樣的結構:

執行時依次判斷各分支條件是否成立,成立則執行當前分支statement,執行完后跳到if外語句;不成立則調到下一分支繼續判斷是否成立,以此類推。不管各分支條件有幾個,其最終都會歸并為一個結果,也就是每個分支只需要判斷最終的條件值是否為true即可,而多個條件計算得到最終值的過程就是普通的邏輯運算。
> __Note:__ 注意elseif與else if,上面介紹的是elseif的編譯,而else if則實際相當于嵌套了一個if,也就是說一個if的分支中包含了另外一個if,在編譯、執行的過程中這兩個是有差別的。
### 4.2.2 switch語句
switch語句與if類似,都是條件語句,很多時候需要將一個變量或者表達式與不同的值進行比較,根據不同的值執行不同的代碼,這種場景下用if、switch都可以實現,但switch相對更加直觀。
switch語法:
```php
switch(expression){
case value1:
statement1;
case value2:
statement2;
...
default:
statementn;
}
```
這里并沒有將break加入到switch的語法中,因為嚴格意義上break并不是switch的一部分,break屬于另外一類單獨的語法:中斷語法,PHP中如果沒有在switch中加break則執行時會從命中的那個case開始一直執行到結束,這與很多其它的語言不同(比如:golang)。
從switch的語法可以看出,switch主要包含兩部分:expression、case list,case list包含多個case,每個case包含value、statement兩部分。expression是一個表達式,但它將在case對比前執行,所以switch最終執行時就是拿expression的值逐個與case的value比較,如果相等則從命中case的statement開始向下執行。
下面看下switch的語法規則:
```c
statement:
...
| T_SWITCH '(' expr ')' switch_case_list { $$ = zend_ast_create(ZEND_AST_SWITCH, $3, $5); }
...
;
switch_case_list:
'{' case_list '}' { $$ = $2; }
| '{' ';' case_list '}' { $$ = $3; }
| ':' case_list T_ENDSWITCH ';' { $$ = $2; }
| ':' ';' case_list T_ENDSWITCH ';' { $$ = $3; }
;
case_list:
/* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_SWITCH_LIST); }
| case_list T_CASE expr case_separator inner_statement_list
{ $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_SWITCH_CASE, $3, $5)); }
| case_list T_DEFAULT case_separator inner_statement_list
{ $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_SWITCH_CASE, NULL, $4)); }
;
case_separator:
':'
| ';'
;
```
從語法解析規則可以看出,switch最終被解析為一個`ZEND_AST_SWITCH`節點,這個節點主要包含兩個子節點:expression、case list,其中expression節點比較簡單,case list節點對應一個`ZEND_AST_SWITCH_LIST`節點,這個節點是一個list,有多個case子節點,每個case節點對應一個`ZEND_AST_SWITCH_CASE`節點,包括value(或expr)、statement兩個子節點,生成的AST如下:

與if不同,switch不會像if那樣依次把每個分支編譯為一組組的condition、statement,而是會先編譯全部case的value表達式,再編譯全部case的statement,編譯過程大致如下:
* (1)首先編譯expression,其最終將得到一個固定的value;
* (2)依次編譯每個case的value,如果value是一個表達式則編譯expression,與(1)相同,執行時其最終也是一個固定的value,每個case編譯一條ZEND_CASE的opcode,除了這條opcode還會編譯出一條ZEND_JMPNZ的opcode,這條opcode用來跳到當前case的statement的開始位置,但是statement在這時還未編譯,所以ZEND_JMPNZ的跳轉值暫不確定;
* (3)編譯完全部case的value后接著從頭開始編譯每個case的statement,編譯前首先設置步驟(2)中ZEND_JMPNZ的跳轉值為當前statement起始位置。
具體編譯過程在`zend_compile_switch()`中,這里不再展開,編譯后的基本結構如下:

執行時首先如果switch的是一個表達式則會首先執行表達式的語句,然后再拿最終的結果逐個與case的值比較,如果case也是一個表達式則也先執行表達式,執行完再與switch的值比較,比較結果如果為true則跳到當前case的statement位置開始順序執行,如果結果為false則繼續向下執行,與下一個case比較,以此類推。
> __Note:__
>
> __(1)__ case不管是表達式還是固定的值其最終比較時是一樣的,如果是表達式則將其執行完以后再作比較,也就是說switch并不支持case多個值的用法,比如:case value1 || value2 : statement,這么寫首先是會執行(value1 || value2),然后把結果與switch的值比較,并不是指switch的值等于value1或value2,這個地方一定要注意,如果想命中多個value只能寫到不同case下
>
> __(2)__ switch的value與case的value比較用的是"==",而不是"==="
- 前言
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本實現
- 1.3.3 FPM的初始化
- 1.3.4 請求處理
- 1.3.5 進程管理
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 第3章 Zend虛擬機
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 3.2 函數實現
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.1 什么是線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.3.1 擴展的構成
- 7.3.2 編譯工具
- 7.3.3 編寫擴展的基本步驟
- 7.3.4 config.m4
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.5.1 全局變量
- 7.5.2 ini配置
- 7.6 函數
- 7.6.1 內部函數注冊
- 7.6.2 函數參數解析
- 7.6.3 引用傳參
- 7.6.4 函數返回值
- 7.6.5 函數調用
- 7.7 zval的操作
- 7.7.1 新生成各類型zval
- 7.7.2 獲取zval的值及類型
- 7.7.3 類型轉換
- 7.7.4 引用計數
- 7.7.5 字符串操作
- 7.7.6 數組操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.1 概述
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- break/continue按標簽中斷語法實現
- defer推遲函數調用語法的實現
- 一起線上事故引發的對PHP超時控制的思考