熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> PHP編程 >> 正文

深入解析php中的foreach問題

2022-06-13   來源: PHP編程 

  php中引入了foreach結構這是一種遍歷數組的簡單方式相比傳統的for循環foreach能夠更加便捷的獲取鍵值對在php之 前foreach僅能用於數組php之後利用foreach還能遍歷對象(詳見遍歷對象)本文中僅討論遍歷數組的情況

foreach雖然簡單不過它可能會出現一些意外的行為特別是代碼涉及引用的情況下
下面列舉了幾種case有助於我們進一步認清foreach的本質
問題

復制代碼 代碼如下:
$arr = array();
foreach($arr as $k => &$v) {
$v = $v * ;
}
// now $arr is array( )
foreach($arr as $k => $v) {
echo "$k" " => " "$v";
}

  
先從簡單的開始如果我們嘗試運行上述代碼就會發現最後輸出為=> => =>
為何不是=> => =>
其實我們可以認為 foreach($arr as $k => $v) 結構隱含了如下操作分別將數組當前的和當前的賦給變量$k和$v具體展開形如

復制代碼 代碼如下:
foreach($arr as $k => $v){
//在用戶代碼執行之前隱含了個賦值操作
$v = currentVal();
$k = currentKey();
//繼續運行用戶代碼
……
}

  
根據上述理論現在我們重新來分析下第一個foreach
遍循環由於$v是一個引用因此$v = &$arr[]$v=$v*相當於$arr[]*因此$arr變成
遍循環$v = &$arr[]$arr變成
遍循環$v = &$arr[]$arr變成
隨後代碼進入了第二個foreach
遍循環隱含操作$v=$arr[]被觸發由於此時$v仍然是$arr[]的引用即相當於$arr[]=$arr[]$arr變成
遍循環$v=$arr[]即$arr[]=$arr[]$arr變成
遍循環$v=$arr[]即$arr[]=$arr[]$arr變成
OK分析完畢
如何解決類似問題呢?php手冊上有一段提醒
Warning : 數組最後一個元素的 $value 引用在 foreach 循環之後仍會保留建議使用unset()來將其銷毀

復制代碼 代碼如下:
$arr = array();
foreach($arr as $k => &$v) {
$v = $v * ;
}
unset($v);
foreach($arr as $k => $v) {
echo "$k" " => " "$v";
}
// 輸出 => => =>

  
從這個問題中我們可以看出引用很有可能會伴隨副作用如果不希望無意識的修改導致數組內容變更最好及時unset掉這些引用
問題

復制代碼 代碼如下:
$arr = array(abc);
foreach($arr as $k => $v) {
echo key($arr) "=>" current($arr);
}
// 打印 =>b =>b =>b

  
這個問題更加詭異按照手冊的說法key和current分別是取數組中當前元素的的鍵值
那為何key($arr)一直是current($arr)一直是b呢?
先用vld查看編譯之後的opcode:

  01.png

我們從第行的ASSIGN指令看起它代表將array(abc)賦值給$arr
由 於$arr為CVarray(abc)為TMP因此ASSIGN指令找到實際執行的函數為 ZEND_ASSIGN_SPEC_CV_TMP_HANDLER這裡需要特別指出CV是PHP之後才增加的一種變量cache它采用數組的 形式來保存zval**被cache住的變量再次使用時無需去查找active符號表而是直接去CV數組中獲取由於數組訪問速度遠超hash表因 而可以提高效率

復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zend_free_op free_op;
zval *value = _get_zval_ptr_tmp(&opline>op EX(Ts) &free_op TSRMLS_CC);

// CV數組中創建出$arr**指針
zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline>op EX(Ts) BP_VAR_W TSRMLS_CC);
if (IS_CV == IS_VAR && !variable_ptr_ptr) {
……
}
else {
// 將array賦值給$arr
value = zend_assign_to_variable(variable_ptr_ptr value TSRMLS_CC);
if (!RETURN_VALUE_UNUSED(&opline>result)) {
AI_SET_PTR(EX_T(opline>resultuvar)var value);
PZVAL_LOCK(value);
}
}
ZEND_VM_NEXT_OPCODE();
}

  
ASSIGN指令完成之後CV數組中被加入zval**指針指針指向實際的array這表示$arr已經被CV緩存了起來02.png

接下來執行數組的循環操作我們來看FE_RESET指令它對應的執行函數為ZEND_FE_RESET_SPEC_CV_HANDLER

復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
if (……) {
……
} else {
// 通過CV數組獲取指向array的指針
array_ptr = _get_zval_ptr_cv(&opline>op EX(Ts) BP_VAR_R TSRMLS_CC);
……
}
……
// 將指向array的指針保存到zend_execute_data>Ts中(Ts用於存放代碼執行期的temp_variable)
AI_SET_PTR(EX_T(opline>resultuvar)var array_ptr);
PZVAL_LOCK(array_ptr);
if (iter) {
……
} else if ((fe_ht = HASH_OF(array_ptr)) != NULL) {
// 重置數組內部指針
zend_hash_internal_pointer_reset(fe_ht);
if (ce) {
……
}
is_empty = zend_hash_has_more_elements(fe_ht) != SUCCESS;

// 設置EX_T(opline>resultuvar)fefe_pos用於保存數組內部指針
zend_hash_get_pointer(fe_ht &EX_T(opline>resultuvar)fefe_pos);
} else {
……
}
……
}

  
這裡主要將個重要的指針存入了zend_execute_data>Ts中:
•EX_T(opline>resultuvar)var 指向array的指針
•EX_T(opline>resultuvar)fefe_pos 指向array內部元素的指針
FE_RESET指令執行完畢之後內存中實際情況如下

  03.png

接下來我們繼續查看FE_FETCH它對應的執行函數為ZEND_FE_FETCH_SPEC_VAR_HANDLER

復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_FE_FETCH_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);

// 注意指針是從EX_T(opline>opuvar)varptr獲取的
zval *array = EX_T(opline>opuvar)varptr;
……

switch (zend_iterator_unwrap(array &iter TSRMLS_CC)) {
default:
case ZEND_ITER_INVALID:
……
case ZEND_ITER_PLAIN_OBJECT: {
……
}
case ZEND_ITER_PLAIN_ARRAY:
fe_ht = HASH_OF(array);

// 特別注意
// FE_RESET指令中將數組內部元素的指針保存在EX_T(opline>opuvar)fefe_pos
// 此處獲取該指針
zend_hash_set_pointer(fe_ht &EX_T(opline>opuvar)fefe_pos);

// 獲取元素的值
if (zend_hash_get_current_data(fe_ht (void **) &value)==FAILURE) {
ZEND_VM_JMP(EX(op_array)>opcodes+opline>opuopline_num);
}
if (use_key) {
key_type = zend_hash_get_current_key_ex(fe_ht &str_key &str_key_len &int_key NULL);
}

// 數組內部指針移動到下一個元素
zend_hash_move_forward(fe_ht);

// 移動之後的指針保存到EX_T(opline>opuvar)fefe_pos
zend_hash_get_pointer(fe_ht &EX_T(opline>opuvar)fefe_pos);
break;
case ZEND_ITER_OBJECT:
……
}

……
}

  
根據FE_FETCH的實現我們大致上明白了foreach($arr as $k => $v)所做的事情它會根據zend_execute_data>Ts的指針去獲取數組元素在獲取成功之後將該指針移動到下一個位置再重新保存

  04.png

  簡單來說由於第一遍循環中FE_FETCH中已經將數組的內部指針移動到了第二個元素所以在foreach內部調用key($arr)和current($arr)時實際上獲取的便是b
那為何會輸出=>b呢?
我們繼續看第行和第行的SEND_REF指令它表示將$arr參數壓棧緊接著一般會使用DO_FCALL指令去調用key和current函數PHP並非被編譯成本地機器碼因此php采用這樣的opcode指令去模擬實際CPU和內存的工作方式
查閱PHP源碼中的SEND_REF

復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
// 從CV中獲取$arr指針的指針
varptr_ptr = _get_zval_ptr_ptr_cv(&opline>op EX(Ts) BP_VAR_W TSRMLS_CC);
……

// 變量分離此處重新copy了一份array專門用於key函數
SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
varptr = *varptr_ptr;
Z_ADDREF_P(varptr);

// 壓棧
zend_vm_stack_push(varptr TSRMLS_CC);
ZEND_VM_NEXT_OPCODE();
}

  
上述代碼中的SEPARATE_ZVAL_TO_MAKE_IS_REF是一個宏

復制代碼 代碼如下:
#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)
if (!PZVAL_IS_REF(*ppzv)) {
SEPARATE_ZVAL(ppzv);
Z_SET_ISREF_PP((ppzv));
}

  
SEPARATE_ZVAL_TO_MAKE_IS_REF的主要作用為如果變量不是一個引用則在內存中copy出一份新的本例中它將array(abc)復制了一份因此變量分離之後的內存為05.png
注意變量分離完成之後CV數組中的指針指向了新copy出來的數據而通過zend_execute_data>Ts中的指針則依然可以獲取舊的數據
接下來的循環就不一一贅述了結合上圖來說
•foreach結構使用的是下方藍色的array會依次遍歷abc
•keycurrent使用的是上方黃色的array它的內部指針永遠指向b
至此我們明白了為何key和current一直返回array的第二個元素由於沒有外部代碼作用於copy出來的array它的內部指針便永遠不會移動
問題

復制代碼 代碼如下:
$arr = array(abc);
foreach($arr as $k => &$v) {
echo key($arr) => current($arr);
}// 打印 =>b =>c =>

  
本題與問題僅有一點區別本題中的foreach使用了引用用VLD查看本題發現與問題代碼編譯出來的opcode一樣因此我們采用問題的跟蹤方法逐步查看opcode對應的實現
首先foreach會調用FE_RESET:

復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
if (opline>extended_value & ZEND_FE_RESET_VARIABLE) {
// 從CV中獲取變量
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline>op EX(Ts) BP_VAR_R TSRMLS_CC);
if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
……
}
else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
……
}
else {
// 針對遍歷array的情況
if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
if (opline>extended_value & ZEND_FE_FETCH_BYREF) {
// 將保存array的zval設置為is_ref
Z_SET_ISREF_PP(array_ptr_ptr);
}
}
array_ptr = *array_ptr_ptr;
Z_ADDREF_P(array_ptr);
}
} else {
……
}
……
}

  
問題中已經分析了一部分FE_RESET的實現這裡需要特別注意本例foreach獲取值采用了引用因此在執行的時候FE_RESET中會進入與上題不同的另一個分支
最終FE_RESET會將array的is_ref設置為true此時內存中只有一份array的數據
接下來分析SEND_REF

復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
// 從CV中獲取$arr指針的指針
varptr_ptr = _get_zval_ptr_ptr_cv(&opline>op EX(Ts) BP_VAR_W TSRMLS_CC);
……

// 變量分離由於此時CV中的變量本身就是一個引用此處不會copy一份新的array
SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
varptr = *varptr_ptr;
Z_ADDREF_P(varptr);

// 壓棧
zend_vm_stack_push(varptr TSRMLS_CC);
ZEND_VM_NEXT_OPCODE();
}

  
宏SEPARATE_ZVAL_TO_MAKE_IS_REF僅僅分離is_ref=false的變量由於之前array已經被設置了is_ref=true因此它不會被拷貝一份副本換句話說此時內存中依然只有一份array數據

  06.png

  上圖解釋了前次循環為何會輸出=>b =>C在第次循環FE_FETCH的時候將指針繼續向前移動

復制代碼 代碼如下:
ZEND_API int zend_hash_move_forward_ex(HashTable *ht HashPosition *pos)
{
HashPosition *current = pos ? pos : &ht>pInternalPointer;
IS_CONSISTENT(ht);
if (*current) {
*current = (*current)>pListNext;
return SUCCESS;
} else
return FAILURE;
}

  
由於此時內部指針已經指向了數組的最後一個元素因此再向前移動會指向NULL將內部指針指向NULL之後我們再對數組調用key和current則分別會返回NULL和false表示調用失敗此時是echo不出字符的
問題

復制代碼 代碼如下:
$arr = array( );
$tmp = $arr;
foreach($tmp as $k => &$v){
$v *= ;
}
var_dump($arr $tmp); // 打印什麼?

  
該題與foreach關系不大不過既然涉及到了foreach就一起拿來討論吧:)
代碼裡首先創建了數組$arr隨後將該數組賦給了$tmp在接下來的foreach循環中對$v進行修改會作用於數組$tmp上但是卻並不作用到$arr
為什麼呢?
這是由於在php中賦值運算是將一個變量的值拷貝到另一個變量中因此修改其中一個並不會影響到另一個
題外話這並不適用於object類型從PHP對象的便總是默認通過引用進行賦值舉例來說

復制代碼 代碼如下:
class A{
public $foo = ;
}
$a = $a = new A;
$a>foo=;
echo $a>foo; // 輸出$a與$a其實為同一個對象的引用

  
回到題目中的代碼現在我們可以確定$tmp=$arr其實是值拷貝整個$arr數組會被再復制一份給$tmp理論上講賦值語句執行完畢之後內存中會有份一樣的數組
也許有同學會疑問如果數組很大豈不是這種操作會很慢?
幸好php有更聰明的處理辦法實際上當$tmp=$arr執行之後內存中依然只有一份array查看php源碼中的zend_assign_to_variable實現(摘自php

復制代碼 代碼如下:
static inline zval* zend_assign_to_variable(zval **variable_ptr_ptr zval *value int is_tmp_var TSRMLS_DC)
{
zval *variable_ptr = *variable_ptr_ptr;
zval garbage;
……
// 左值為object類型
if (Z_TYPE_P(variable_ptr) == IS_OBJECT && Z_OBJ_HANDLER_P(variable_ptr set)) {
……
}
// 左值為引用的情況
if (PZVAL_IS_REF(variable_ptr)) {
……
} else {
// 左值refcount__gc=的情況
if (Z_DELREF_P(variable_ptr)==) {
……
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(*variable_ptr_ptr);
// 非臨時變量
if (!is_tmp_var) {
if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > ) {
ALLOC_ZVAL(variable_ptr);
*variable_ptr_ptr = variable_ptr;
*variable_ptr = *value;
Z_SET_REFCOUNT_P(variable_ptr );
zval_copy_ctor(variable_ptr);
} else {
// $tmp=$arr會運行到這裡
// value為指向$arr裡實際array數據的指針variable_ptr_ptr為$tmp裡指向數據指針的指針
// 僅僅是復制指針並沒有真正拷貝實際的數組
*variable_ptr_ptr = value;
// value的refcount__gc值+本例中refcount__gc為Z_ADDREF_P之後為
Z_ADDREF_P(value);
}
} else {
……
}
}
Z_UNSET_ISREF_PP(variable_ptr_ptr);
}
return *variable_ptr_ptr;
}

  
可見$tmp = $arr的本質就是將array的指針進行復制然後將array的refcount自動加用圖表達出此時的內存依然只有一份array數組
07.png
既然只有一份array那foreach循環中修改$tmp的時候為何$arr沒有跟著改變?
繼續看PHP源碼中的ZEND_FE_RESET_SPEC_CV_HANDLER函數這是一個OPCODE HANDLER它對應的OPCODE為FE_RESET該函數負責在foreach開始之前將數組的內部指針指向其第一個元素

復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zval *array_ptr **array_ptr_ptr;
HashTable *fe_ht;
zend_object_iterator *iter = NULL;
zend_class_entry *ce = NULL;
zend_bool is_empty = ;
// 對變量進行FE_RESET
if (opline>extended_value & ZEND_FE_RESET_VARIABLE) {
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline>op EX(Ts) BP_VAR_R TSRMLS_CC);
if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
……
}
// foreach一個object
else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
……
}
else {
// 本例會進入該分支
if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
// 注意此處的SEPARATE_ZVAL_IF_NOT_REF
// 它會重新復制一個數組出來
// 真正分離$tmp和$arr變成了內存中的個數組
SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
if (opline>extended_value & ZEND_FE_FETCH_BYREF) {
Z_SET_ISREF_PP(array_ptr_ptr);
}
}
array_ptr = *array_ptr_ptr;
Z_ADDREF_P(array_ptr);
}
} else {
……
}

// 重置數組內部指針
……
}

  
從代碼中可以看出真正執行變量分離並不是在賦值語句執行的時候而是推遲到了使用變量的時候這也是Copy On Write機制在PHP中的實現
FE_RESET之後內存的變化如下
08.png

上 圖解釋了為何foreach並不會對原來的$arr產生影響至於ref_count以及is_ref的變化情況感興趣的同學可以詳細閱讀 ZEND_FE_RESET_SPEC_CV_HANDLER和ZEND_SWITCH_FREE_SPEC_VAR_HANDLER的具體實現(均位於 phpsrc/zend/zend_vm_executeh中)本文不做詳細剖析:)


From:http://tw.wingwit.com/Article/program/PHP/201311/20993.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.