foreach使用引用時的一個坑-foreach原始碼分析
背景描述
先看一段程式碼。
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
foreach ($arr as &$val) {
echo $val;
}
foreach ($arr as $val) {
echo $val;
}
print_r($arr);
想一下應該輸出什麼呢?
執行一下指令碼,真實結果和你想的是否一致呢?
在foreach中使用了引用後再次foreach發現$arr[`less`]的值變成了54,常規理解應該是23才對。
猜測可能是因為使用引用導致該值變為54 但本著知其然更要知其所以然 我們一起追一下php原始碼 是什麼原因導致的
環境準備
工欲善其事必先利其器,先下載除錯工具及原始碼
安裝Visual Studio
下載Visual Studio 2017,並安裝
下載地址:https://www.visualstudio.com/zh-hans/downloads/
下載php原始碼
因神馬前端目前使用php7.0,因此下載php7.0的最新版 7.0.27為研究物件
下載地址:http://cn2.php.net/distributions/php-7.0.27.tar.bz2
建立解決方案
根據已有目錄生成解決方案
建立成功後如下圖所示
原始碼追蹤
詞法分析階段
搜尋關鍵字foreach
可以在zend_language_parser.c 中看到, 語法解析時 foreach會當做T_FOREACH
在zend_language_parser.y可以看到語法解析的具體方式
ZEND_AST_FOREACH
查詢zend_ast_create
zend_ast.c中:
zend_ast_create 函式是建立一個抽象語法樹(abstract syntax tree)返回的zend_ast結構如下:
具體的賦值操作如下:
編譯生成opcode
接下來在zend_compile.c中根據抽象語法樹生成opcode:
通過上圖及語法解析的分析可知,foreach在編譯階段會生成如上圖的四個zend_ast節點,分別表示:要遍歷的陣列或物件expr_ast,要遍歷的value value_ast,要遍歷的key key_ast,迴圈體stmt_ast
如:
$arr = [1, 2, 3];
foreach ($arr as $key => $val) {
echo $val;
}
expr_ast 是可理解為是$arr編譯時對應的ast結構
value_ast對應$val
key_ast對應$key
stmt_ast對應"echo $val;"
copy一份要遍歷的陣列或物件,如果是引用則把原陣列或物件設為引用型別
如:
foreach ($arr as $k => $v) {
echo $v;
}
copy一份$arr用於遍歷,從arData的首元素起,把bucket.zval.value賦值給$v,把bucket.h或key賦值給$k,然後將下一個元素的位置記錄在zval.u2.fe_iter_idx中,下次遍歷從該位置開始,當u2.fe_iter_idex到了arData的末尾則遍歷結束並銷燬copy的$arr副本
如果$v是引用 則在迴圈前,將原$arr設定為引用型別 即:
foreach ($arr as $k => &$v) {
echo $v;
}
- 編譯copy的陣列、物件操作的指令:增加一條opcode指令 ZEND_FE_RESET_R(如果value是引用則用ZEND_FE_RESET_RW) 。執行時如果發現遍歷的不是陣列、物件 則丟擲一個warning,然後跳出迴圈。
- 編譯fetch陣列、物件當前單元key 、value的opcode : ZEND_FE_FETCH_R(如果value是引用則用ZEND_FE_FETCH_RW)。此opcode需要知道當遍歷到達陣列末尾時跳出遍歷的位置。此外還會對key和value分配他們在記憶體中的位置,如果value不是一CV個變數,還會編譯其它操作的opcode
- 如果定義了key,則會編譯一條opcode,對key進行賦值
- 編譯迴圈體statement
- 編譯跳回遍歷開始時的opcode,一次遍歷結束後跳到步驟2編譯的opcode進行下次遍歷
- 設定步驟1、2兩條opcode如果出錯要跳到的opcode
- 結束迴圈 編譯ZEND_FE_FREE用於釋放1中copy的陣列或物件
結論分析
編譯後的結構
執行時步驟:
- 執行ZEND_FE_RESET_R,過程上面已經介紹了;
- 執行ZEND_FE_FETCH_R,此opcode的操作主要有三個:檢查遍歷位置是否到達末尾、將陣列元素的value賦值給$value、將陣列元素的key賦值給一個臨時變數(注意與value不同);
- 如果定義了key則執行ZEND_ASSIGN,將key的值從臨時變數賦值給$key,否則跳到步驟(4);
- 執行迴圈體的statement;
- 執行ZEND_JMPNZ跳回步驟(2);
- 遍歷結束後執行ZEND_FE_FREE釋放陣列。
根據上面的分析可知:賦值的核心操作是ZEND_FE_FETCH_R和ZEND_FE_FETCH_RW
等價關係
最開始舉的例子可等價於
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
$val = &$arr[`jack`];
$val = &$arr[`tom`];
$val = &$arr[`marry`];
$val = &$arr[`less`];
$val = $arr[`jack`];
$val = $arr[`tom`];
$val = $arr[`marry`];
$val = $arr[`less`];
print_r($arr);
等價於:
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
$val = &$arr[`less`]; (23)
$val = $arr[`marry`]; (54,並且此時因為引用 $arr[`less`]也變為54了)
$val = $arr[`less`]; (54)
print_r($arr);
建議
因此 為了避免出現不必要的錯誤,建議在使用完&後,unset掉變數以取消對地址的引用
思維發散
針對以上情況,如果不取消對變數的引用,而是將陣列賦值給一個新的變數再foreach。是否可行?
普通變數的引用
先看一段程式碼:
<?php
$str = `20`;
$c = &$str;
$a = $str;
$c = 30;
var_dump($a);
輸出20 沒有任何問題
陣列整體引用
如果換成陣列:
<?php
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
$b = &$arr;
$a = $arr;
$b[`jack`] = 30;
var_dump($a);
還是20 符合預期
陣列元素引用
但如果這樣呢:
<?php
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
$b = &$arr[`jack`];
$a = $arr;
$b = 30;
var_dump($a)
值卻變成了30
xdebug_debug_zval除錯
我們加上xdebug_debug_zval看看發生了什麼
<?
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
$b = &$arr;
$a = $arr;
$b[`jack`] = 30;
var_dump($a);
xdebug_debug_zval(`a`);
xdebug_debug_zval(`arr`);
可以看出,直接引用陣列, $b = &$arr, $arr 的is_ref是1,refcount是2, 給$a = $arr時,發生分離,$a 與$arr指向不同的zval,$b 與 $arr指向相同的zval,因此給$b[`jack`] = 30, $a的值不會發生改變
<?
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
$b = &$arr[`jack`];
$a = $arr;
$b = 30;
var_dump($a);
xdebug_debug_zval(`a`);
xdebug_debug_zval(`arr`)
可以看出,對陣列中一個元素引用時,陣列的is_ref是0,因為$a = $arr 因此refcount是2 ,指向同一個zval,改變$b的值時,因為$arr[`jack`]是一個引用,zval的值改變,$a和$arr的zval相同,$a[`jack`]也變為30
結論
同理可以回答最開始提出的疑問:如果我不取消對變數的引用,而是將陣列賦值給一個新的變數再foreach。是否可行?答:不行。
<?
$arr = [
`jack` => `20`,
`tom` => `21`,
`marry` => `54`,
`less` => `23`
];
foreach ($arr as &$val) {
echo $val;
}
$a = $arr;
foreach ($a as $val) {
echo $val;
}
print_r($a);
因為$arr與$a指向同一份zval,還是會出現$a[`less`] = 54的結果。因此,在foreach使用完&後,還是unset掉變數 取消對地址的引用再進行下一步操作吧
參考文獻:
[https://github.com/pangudashu/php7-internal/blob/master/4/loop.md
](https://github.com/pangudashu/php7-internal/blob/master/4/loop.md)
相關文章
- 使用 foreach 使用引用變數需要注意的問題變數
- php中foreach的使用PHP
- java中 foreach 的使用Java
- Mybatis中foreach的使用MyBatis
- 原始碼分析 Mybatis 的 foreach 為什麼會出現效能問題原始碼MyBatis
- Java 8 forEach使用Java
- PHP 引用變數名與 foreach 中 $val 同名時發生的變化PHP變數
- JavaScript forEach()JavaScript
- Iterator & foreach
- mybatis關於list的foreach的使用MyBatis
- java中的forEachJava
- Mybatis的 foreach 標籤使用方法.MyBatis
- MyBatis學習——foreach標籤的使用MyBatis
- Foreach巢狀Foreach速度慢優化方案巢狀優化
- JS forEach()第二個引數JS
- JavaScript中foreachJavaScript
- C#效率優化(3)-- 使用foreach時避免裝箱C#優化
- 哈哈,PHP中使用foreach和引用導致程式BUG及解決辦法PHP
- mybatis foreach迴圈MyBatis
- MyBatis foreach標籤MyBatis
- MyBatis 使用foreach與其他方式的時候引數傳遞方式MyBatis
- javascript forEach無法break,使用every代替JavaScript
- JS:解析forEach()、map()原始碼及淺談兩者之間的區別JS原始碼
- PHP 中的 foreach 工作原理PHP
- 14,javase程式碼實戰-迴圈控制——foreach迴圈的使用(五)Java
- C#中foreach基礎使用方法C#
- jstl forEach遍歷JS
- 關於在forEach中使用await的問題AI
- JAVA中的foreach怎麼用Java
- 兩個大陣列foreach,找出相同的key數量,所用的時間對比陣列
- JS陣列迴圈的效能和效率分析(for、while、forEach、map、for of)JS陣列While
- C#併發實戰Parallel.ForEach使用C#Parallel
- MyBatis Batch Update Exception使用foreach批量update出錯MyBatisException
- forEach()相容所有瀏覽器瀏覽器
- 深入理解PHP之foreachPHP
- C# List.ForEach 方法C#
- 你可以終止 forEach 嗎?
- 在Java8的foreach()中不能break,如果需要continue時,可以使用returnJava