程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> php中的foreach問題

php中的foreach問題

編輯:關於PHP編程

前言:

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

foreach雖然簡單,不過它可能會出現一些意外的行為,特別是代碼涉及引用的情況下。

下面列舉了幾種case,有助於我們進一步認清foreach的本質。

問題1:

$arr = array(,,($arr  $k => &= $v * 

($arr  $k =>, , 

先從簡單的開始,如果我們嘗試運行上述代碼,就會發現最後輸出為0=>2  1=>4  2=>4

為何不是0=>2  1=>4  2=>6 ?

其實,我們可以認為 ( => 隱含了如下操作,分別將數組當前的'鍵'和當前的'值'賦給變量$k和$v。具體展開形如:

(   =>  =     =
    

根據上述理論,現在我們重新來分析下第一個foreach:

第1遍循環,由於$v是一個引用,因此$v = &$arr[0],$v=$v*2相當於$arr[0]*2,因此$arr變成2,2,3

第2遍循環,$v = &$arr[1],$arr變成2,4,3

第3遍循環,$v = &$arr[2],$arr變成2,4,6

隨後代碼進入了第二個foreach:

第1遍循環,隱含操作$v=$arr[0]被觸發,由於此時$v仍然是$arr[2]的引用,即相當於$arr[2]=$arr[0],$arr變成2,4,2

第2遍循環,$v=$arr[1],即$arr[2]=$arr[1],$arr變成2,4,4

第3遍循環,$v=$arr[2],即$arr[2]=$arr[2],$arr變成2,4,4

OK,分析完畢。

如何解決類似問題呢?php手冊上有一段提醒:

Warning : 數組最後一個元素的 $value 引用在 foreach 循環之後仍會保留。建議使用unset()來將其銷毀。
 = (1,2,3(   => & =  * 2((   =>  "", " => ", ""

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

問題2:

 = ('a','b','c'(   =>  (), "=>", ()
// 打印 1=>b 1=>b 1=>b

這個問題更加詭異。按照手冊的說法,key和current分別是取數組中當前元素的的鍵值。

那為何key($arr)一直是1,current($arr)一直是b呢?

先用vld查看編譯之後的opcode:

我們從第3行的ASSIGN指令看起,它代表將array('a','b','c')賦值給$arr。

由於$arr為CV,array('a','b','c')為TMP,因此ASSIGN指令找到實際執行的函數為ZEND_ASSIGN_SPEC_CV_TMP_HANDLER。這裡需要特別指出,CV是PHP5.1之後才增加的一種變量cache,它采用數組的形式來保存zval**,被cache住的變量再次使用時無需去查找active符號表,而是直接去CV數組中獲取,由於數組訪問速度遠超hash表,因而可以提高效率。

 *opline =*value = _get_zval_ptr_tmp(&opline->op2, EX(Ts), &
    zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline-> (IS_CV == IS_VAR && !
         value = zend_assign_to_variable(variable_ptr_ptr, value,  (!RETURN_VALUE_UNUSED(&opline->->result.u.).

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

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

 
        array_ptr = _get_zval_ptr_cv(&opline->array的指針保存到zend_execute_data->Ts中(Ts用於存放代碼執行期的temp_variable)
    AI_SET_PTR(EX_T(opline->result.u.).  ((fe_ht = HASH_OF(array_ptr)) !=
= zend_hash_has_more_elements(fe_ht) !=
        zend_hash_get_pointer(fe_ht, &EX_T(opline->result.u.

這裡主要將2個重要的指針存入了中:

  • EX_T(opline->result.u.var).var ---- 指向array的指針
  • EX_T(opline->result.u.var).fe.fe_pos ---- 指向array內部元素的指針

FE_RESET指令執行完畢之後,內存中實際情況如下:

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

 *opline =
    zval *array = EX_T(opline->op1.u.). (zend_iterator_unwrap(array, &=
            zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.
             (zend_hash_get_current_data(fe_ht, ( **) &value)==->opcodes+opline->= zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len, &int_key, 

            zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.

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

簡單來說,由於第一遍循環中FE_FETCH中已經將數組的內部指針移動到了第二個元素,所以在foreach內部調用key($arr)和current($arr)時,實際上獲取的便是1和'b'。

那為何會輸出3遍1=>b呢?

我們繼續看第9行和第13行的SEND_REF指令,它表示將$arr參數壓棧。緊接著一般會使用DO_FCALL指令去調用key和current函數。PHP並非被編譯成本地機器碼,因此php采用這樣的opcode指令去模擬實際CPU和內存的工作方式。

查閱PHP源碼中的SEND_REF:

 
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline->
= *

上述代碼中的

 SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)    \
     (!PZVAL_IS_REF(*

的主要作用為,如果變量不是一個引用,則在內存中copy出一份新的。本例中它將array('a','b','c')復制了一份。因此變量分離之後的內存為:

注意,變量分離完成之後,CV數組中的指針指向了新copy出來的數據,而通過中的指針則依然可以獲取舊的數據。

接下來的循環就不一一贅述了,結合上圖來說:

  • foreach結構使用的是下方藍色的array,會依次遍歷a,b,c
  • key、current使用的是上方黃色的array,它的內部指針永遠指向b

至此我們明白了為何key和current一直返回array的第二個元素,由於沒有外部代碼作用於copy出來的array,它的內部指針便永遠不會移動。

問題3:

 = ('a','b','c'(   => & (), '=>', ()


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

首先foreach會調用FE_RESET:

  (opline->extended_value &
        array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline-> (array_ptr_ptr == NULL || array_ptr_ptr == &  (Z_TYPE_PP(array_ptr_ptr) ==
             (Z_TYPE_PP(array_ptr_ptr) == (opline->extended_value &
= *

問題2中已經分析了一部分FE_RESET的實現。這裡需要特別注意,本例foreach獲取值采用了引用,因此在執行的時候FE_RESET中會進入與上題不同的另一個分支。

最終,FE_RESET會將array的is_ref設置為true,此時內存中只有一份array的數據。

接下來分析SEND_REF:

 
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline->
= *

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

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

ZEND_API  zend_hash_move_forward_ex(HashTable *ht, HashPosition **current = pos ? pos : &ht-> (**current = (*current)->
        

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

 問題4:

 = (1, 2, 3 = (  => & *= 2(, ); 

該題與foreach關系不大,不過既然涉及到了foreach,就一起拿來討論吧:)

代碼裡首先創建了數組$arr,隨後將該數組賦給了$tmp,在接下來的foreach循環中,對$v進行修改會作用於數組$tmp上,但是卻並不作用到$arr。

為什麼呢?

這是由於在php中,賦值運算是將一個變量的值拷貝到另一個變量中,因此修改其中一個,並不會影響到另一個。

題外話:這並不適用於object類型,從PHP5起,對象的便總是默認通過引用進行賦值,舉例來說:

  = 1 =  = ->foo=100 ->foo; 

回到題目中的代碼,現在我們可以確定$tmp=$arr其實是值拷貝,整個$arr數組會被再復制一份給$tmp。理論上講,賦值語句執行完畢之後,內存中會有2份一樣的數組。

也許有同學會疑問,如果數組很大,豈不是這種操作會很慢?

幸好php有更聰明的處理辦法。實際上,當$tmp=$arr執行之後,內存中依然只有一份array。查看php源碼中的zend_assign_to_variable實現(摘自php5.3.26):

 inline zval* zend_assign_to_variable(zval **variable_ptr_ptr, zval *value, *variable_ptr = *
     (Z_TYPE_P(variable_ptr) == IS_OBJECT && Z_OBJ_HANDLER_P(variable_ptr, 
    
         (Z_DELREF_P(variable_ptr)==*
             (! (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > *variable_ptr_ptr =*variable_ptr = *                    // value為指向$arr裡實際array數據的指針,variable_ptr_ptr為$tmp裡指向數據指針的指針
                    
                    *variable_ptr_ptr =
 *

可見$tmp = $arr的本質就是將array的指針進行復制,然後將array的自動加1.用圖表達出此時的內存,依然只有一份array數組:

既然只有一份array,那foreach循環中修改$tmp的時候,為何$arr沒有跟著改變?

繼續看PHP源碼中的ZEND_FE_RESET_SPEC_CV_HANDLER函數,這是一個OPCODE HANDLER,它對應的OPCODE為FE_RESET。該函數負責在foreach開始之前,將數組的內部指針指向其第一個元素。

*opline =*array_ptr, ****iter = *ce = = 0
     (opline->extended_value &= _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), (array_ptr_ptr ==  || array_ptr_ptr == &
          (Z_TYPE_PP(array_ptr_ptr) == 
             (Z_TYPE_PP(array_ptr_ptr) ==                 // 它會重新復制一個數組出來
                // 真正分離$tmp和$arr,變成了內存中的2個數組
 (opline->extended_value &= *

從代碼中可以看出,真正執行變量分離並不是在賦值語句執行的時候,而是推遲到了使用變量的時候,這也是Copy On Write機制在PHP中的實現。

FE_RESET之後,內存的變化如下:

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved