<pre id="bbfd9"><del id="bbfd9"><dfn id="bbfd9"></dfn></del></pre>

          <ruby id="bbfd9"></ruby><p id="bbfd9"><mark id="bbfd9"></mark></p>

          <p id="bbfd9"></p>

          <p id="bbfd9"><cite id="bbfd9"></cite></p>

            <th id="bbfd9"><form id="bbfd9"><dl id="bbfd9"></dl></form></th>

            <p id="bbfd9"><cite id="bbfd9"></cite></p><p id="bbfd9"></p>
            <p id="bbfd9"><cite id="bbfd9"><progress id="bbfd9"></progress></cite></p>
            php語言

            解析php中的foreach問題

            時間:2025-02-17 00:02:02 php語言 我要投稿
            • 相關推薦

            解析php中的foreach問題

              php4中引入了foreach結構,這是一種遍歷數組的簡單方式。相比傳統的for循環,foreach能夠更加便捷的獲取鍵值對。在php5之 前,foreach僅能用于數組;php5之后,利用foreach還能遍歷對象。以下是小編為大家搜索整理的解析php中的foreach問題,希望能給大家帶來幫助!更多精彩內容請及時關注我們應屆畢業生考試網!

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

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

              問題1:

              復制代碼 代碼如下:

              $arr = array(1,2,3);

              foreach($arr as $k => &$v) {

              $v = $v * 2;

              }

              // now $arr is array(2, 4, 6)

              foreach($arr as $k => $v) {

              echo "$k", " => ", "$v";

              }

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

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

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

              復制代碼 代碼如下:

              foreach($arr as $k => $v){

              //在用戶代碼執行之前隱含了2個賦值操作

              $v = currentVal();

              $k = currentKey();

              //繼續運行用戶代碼

              ……

              }

              根據上述理論,現在我們重新來分析下第一個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()來將其銷毀。

              復制代碼 代碼如下:

              $arr = array(1,2,3);

              foreach($arr as $k => &$v) {

              $v = $v * 2;

              }

              unset($v);

              foreach($arr as $k => $v) {

              echo "$k", " => ", "$v";

              }

              // 輸出 0=>2 1=>4 2=>6

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

              問題2:

              復制代碼 代碼如下:

              $arr = array('a','b','c');

              foreach($arr as $k => $v) {

              echo key($arr), "=>", current($arr);

              }

              // 打印 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表,因 而可以提高效率。

              復制代碼 代碼如下:

              static int ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

              {

              zend_op *opline = EX(opline);

              zend_free_op free_op2;

              zval *value = _get_zval_ptr_tmp(&opline->op2, EX(Ts), &free_op2 TSRMLS_CC);

              // CV數組中創建出$arr**指針

              zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, 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, 1 TSRMLS_CC);

              if (!RETURN_VALUE_UNUSED(&opline->result)) {

              AI_SET_PTR(EX_T(opline->result.u.var).var, value);

              PZVAL_LOCK(value);

              }

              }

              ZEND_VM_NEXT_OPCODE();

              }

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

              接下來執行數組的循環操作,我們來看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->op1, EX(Ts), BP_VAR_R TSRMLS_CC);

              ……

              }

              ……

              // 將指向array的指針保存到zend_execute_data->Ts中(Ts用于存放代碼執行期的temp_variable)

              AI_SET_PTR(EX_T(opline->result.u.var).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->result.u.var).fe.fe_pos用于保存數組內部指針

              zend_hash_get_pointer(fe_ht, &EX_T(opline->result.u.var).fe.fe_pos);

              } else {

              ……

              }

              ……

              }

              這里主要將2個重要的指針存入了zend_execute_data->Ts中:

              •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:

              復制代碼 代碼如下:

              static int ZEND_FASTCALL ZEND_FE_FETCH_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

              {

              zend_op *opline = EX(opline);

              // 注意指針是從EX_T(opline->op1.u.var).var.ptr獲取的

              zval *array = EX_T(opline->op1.u.var).var.ptr;

              ……

              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->op1.u.var).fe.fe_pos

              // 此處獲取該指針

              zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);

              // 獲取元素的值

              if (zend_hash_get_current_data(fe_ht, (void **) &value)==FAILURE) {

              ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_num);

              }

              if (use_key) {

              key_type = zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len, &int_key, 1, NULL);

              }

              // 數組內部指針移動到下一個元素

              zend_hash_move_forward(fe_ht);

              // 移動之后的指針保存到EX_T(opline->op1.u.var).fe.fe_pos

              zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);

              break;

              case ZEND_ITER_OBJECT:

              ……

              }

              ……

              }

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

              簡單來說,由于第一遍循環中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:

              復制代碼 代碼如下:

              static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

              {

              ……

              // 從CV中獲取$arr指針的指針

              varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, 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('a','b','c')復制了一份。因此變量分離之后的內存為:

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

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

              •foreach結構使用的是下方藍色的array,會依次遍歷a,b,c

              •key、current使用的是上方黃色的array,它的內部指針永遠指向b

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

              問題3:

              復制代碼 代碼如下:

              $arr = array('a','b','c');

              foreach($arr as $k => &$v) {

              echo key($arr), '=>', current($arr);

              }// 打印 1=>b 2=>c =>

              本題與問題2僅有一點區別:本題中的foreach使用了引用。用VLD查看本題,發現與問題2代碼編譯出來的opcode一樣。因此我們采用問題2的跟蹤方法,逐步查看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->op1, 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 {

              ……

              }

              ……

              }

              問題2中已經分析了一部分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->op1, 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數據。

              上圖解釋了前2次循環為何會輸出1=>b 2=>C。在第3次循環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不出字符的。

              問題4:

              復制代碼 代碼如下:

              $arr = array(1, 2, 3);

              $tmp = $arr;

              foreach($tmp as $k => &$v){

              $v *= 2;

              }

              var_dump($arr, $tmp); // 打印什么?

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

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

              為什么呢?

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

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

              復制代碼 代碼如下:

              class A{

              public $foo = 1;

              }

              $a1 = $a2 = new A;

              $a1->foo=100;

              echo $a2->foo; // 輸出100,$a1與$a2其實為同一個對象的引用

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

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

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

              復制代碼 代碼如下:

              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=1的情況

              if (Z_DELREF_P(variable_ptr)==0) {

              ……

              } else {

              GC_ZVAL_CHECK_POSSIBLE_ROOT(*variable_ptr_ptr);

              // 非臨時變量

              if (!is_tmp_var) {

              if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {

              ALLOC_ZVAL(variable_ptr);

              *variable_ptr_ptr = variable_ptr;

              *variable_ptr = *value;

              Z_SET_REFCOUNT_P(variable_ptr, 1);

              zval_copy_ctor(variable_ptr);

              } else {

              // $tmp=$arr會運行到這里,

              // value為指向$arr里實際array數據的指針,variable_ptr_ptr為$tmp里指向數據指針的指針

              // 僅僅是復制指針,并沒有真正拷貝實際的數組

              *variable_ptr_ptr = value;

              // value的refcount__gc值+1,本例中refcount__gc為1,Z_ADDREF_P之后為2

              Z_ADDREF_P(value);

              }

              } else {

              ……

              }

              }

              Z_UNSET_ISREF_PP(variable_ptr_ptr);

              }

              return *variable_ptr_ptr;

              }

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

              既然只有一份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 = 0;

              // 對變量進行FE_RESET

              if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {

              array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, 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,變成了內存中的2個數組

              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之后,內存的變化如下:

              上 圖解釋了為何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中),本文不做詳細剖析:)

            【解析php中的foreach問題】相關文章:

            深入解析PHP中foreach語句控制數組循環08-06

            php循環語句for()與foreach()用法區別03-08

            php中死鎖問題剖析03-25

            解析php安全性問題中的Null字符問題02-11

            PHP中浮點數的計算問題08-09

            php中zend相對路徑問題03-02

            在PHP中操作MySQL要注意哪些問題07-18

            PHP use類文件中的命名空間問題02-13

            解析托福詞匯中前綴的問題分析08-06

                    <pre id="bbfd9"><del id="bbfd9"><dfn id="bbfd9"></dfn></del></pre>

                    <ruby id="bbfd9"></ruby><p id="bbfd9"><mark id="bbfd9"></mark></p>

                    <p id="bbfd9"></p>

                    <p id="bbfd9"><cite id="bbfd9"></cite></p>

                      <th id="bbfd9"><form id="bbfd9"><dl id="bbfd9"></dl></form></th>

                      <p id="bbfd9"><cite id="bbfd9"></cite></p><p id="bbfd9"></p>
                      <p id="bbfd9"><cite id="bbfd9"><progress id="bbfd9"></progress></cite></p>
                      飘沙影院