溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么在php中的foreach問題進行處理

發(fā)布時間:2021-01-14 14:59:35 來源:億速云 閱讀:207 作者:Leah 欄目:開發(fā)技術(shù)

本篇文章給大家分享的是有關(guān)怎么在php中的foreach問題進行處理,小編覺得挺實用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

前言:
php4中引入了foreach結(jié)構(gòu),這是一種遍歷數(shù)組的簡單方式。相比傳統(tǒng)的for循環(huán),foreach能夠更加便捷的獲取鍵值對。在php5之前,foreach僅能用于數(shù)組;php5之后,利用foreach還能遍歷對象(詳見:遍歷對象)。本文中僅討論遍歷數(shù)組的情況。

foreach雖然簡單,不過它可能會出現(xiàn)一些意外的行為,特別是代碼涉及引用的情況下。
下面列舉了幾種case,有助于我們進一步認(rèn)清foreach的本質(zhì)。
問題1:

復(fù)制代碼 代碼如下:


$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";
}


先從簡單的開始,如果我們嘗試運行上述代碼,就會發(fā)現(xiàn)最后輸出為0=>2  1=>4  2=>4 。
為何不是0=>2  1=>4  2=>6 ?
其實,我們可以認(rèn)為 foreach($arr as $k => $v) 結(jié)構(gòu)隱含了如下操作,分別將數(shù)組當(dāng)前的'鍵'和當(dāng)前的'值'賦給變量$k和$v。具體展開形如:

復(fù)制代碼 代碼如下:


foreach($arr as $k => $v){
    //在用戶代碼執(zhí)行之前隱含了2個賦值操作
    $v = currentVal();
    $k = currentKey();
    //繼續(xù)運行用戶代碼
    ……
}


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

復(fù)制代碼 代碼如下:


$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


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

復(fù)制代碼 代碼如下:


$arr = array('a','b','c');
foreach($arr as $k => $v) {
    echo key($arr), "=>", current($arr);
}
// 打印 1=>b 1=>b 1=>b


這個問題更加詭異。按照手冊的說法,key和current分別是取數(shù)組中當(dāng)前元素的的鍵值。
那為何key($arr)一直是1,current($arr)一直是b呢?
先用vld查看編譯之后的opcode:怎么在php中的foreach問題進行處理

我們從第3行的ASSIGN指令看起,它代表將array('a','b','c')賦值給$arr。
由于$arr為CV,array('a','b','c')為TMP,因此ASSIGN指令找到實際執(zhí)行的函數(shù)為ZEND_ASSIGN_SPEC_CV_TMP_HANDLER。這里需要特別指出,CV是PHP5.1之后才增加的一種變量cache,它采用數(shù)組的形式來保存zval**,被cache住的變量再次使用時無需去查找active符號表,而是直接去CV數(shù)組中獲取,由于數(shù)組訪問速度遠(yuǎn)超hash表,因而可以提高效率。

復(fù)制代碼 代碼如下:


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數(shù)組中創(chuàng)建出$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數(shù)組中被加入zval**指針,指針指向?qū)嶋H的array,這表示$arr已經(jīng)被CV緩存了起來。怎么在php中的foreach問題進行處理

接下來執(zhí)行數(shù)組的循環(huán)操作,我們來看FE_RESET指令,它對應(yīng)的執(zhí)行函數(shù)為ZEND_FE_RESET_SPEC_CV_HANDLER:

復(fù)制代碼 代碼如下:


static int ZEND_FASTCALL  ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    if (……) {
        ……
    } else {
        // 通過CV數(shù)組獲取指向array的指針
        array_ptr = _get_zval_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
        ……
    }
    ……
    // 將指向array的指針保存到zend_execute_data->Ts中(Ts用于存放代碼執(zhí)行期的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) {
        // 重置數(shù)組內(nèi)部指針
        zend_hash_internal_pointer_reset(fe_ht);
        if (ce) {
            ……
        }
        is_empty = zend_hash_has_more_elements(fe_ht) != SUCCESS;

        // 設(shè)置EX_T(opline->result.u.var).fe.fe_pos用于保存數(shù)組內(nèi)部指針
        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內(nèi)部元素的指針
FE_RESET指令執(zhí)行完畢之后,內(nèi)存中實際情況如下:

怎么在php中的foreach問題進行處理

接下來我們繼續(xù)查看FE_FETCH,它對應(yīng)的執(zhí)行函數(shù)為ZEND_FE_FETCH_SPEC_VAR_HANDLER:

復(fù)制代碼 代碼如下:


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指令中將數(shù)組內(nèi)部元素的指針保存在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);
            }

            // 數(shù)組內(nèi)部指針移動到下一個元素
            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:
            ……
    }

    ……
}


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

怎么在php中的foreach問題進行處理

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

復(fù)制代碼 代碼如下:


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函數(shù)
    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是一個宏:

復(fù)制代碼 代碼如下:


#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的主要作用為,如果變量不是一個引用,則在內(nèi)存中copy出一份新的。本例中它將array('a','b','c')復(fù)制了一份。因此變量分離之后的內(nèi)存為:怎么在php中的foreach問題進行處理
注意,變量分離完成之后,CV數(shù)組中的指針指向了新copy出來的數(shù)據(jù),而通過zend_execute_data->Ts中的指針則依然可以獲取舊的數(shù)據(jù)。
接下來的循環(huán)就不一一贅述了,結(jié)合上圖來說:
?foreach結(jié)構(gòu)使用的是下方藍(lán)色的array,會依次遍歷a,b,c
?key、current使用的是上方黃色的array,它的內(nèi)部指針永遠(yuǎn)指向b
至此我們明白了為何key和current一直返回array的第二個元素,由于沒有外部代碼作用于copy出來的array,它的內(nèi)部指針便永遠(yuǎn)不會移動。
問題3:

復(fù)制代碼 代碼如下:


$arr = array('a','b','c');
foreach($arr as $k => &$v) {
    echo key($arr), '=>', current($arr);
}// 打印 1=>b 2=>c =>


本題與問題2僅有一點區(qū)別:本題中的foreach使用了引用。用VLD查看本題,發(fā)現(xiàn)與問題2代碼編譯出來的opcode一樣。因此我們采用問題2的跟蹤方法,逐步查看opcode對應(yīng)的實現(xiàn)。
首先foreach會調(diào)用FE_RESET:

復(fù)制代碼 代碼如下:


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設(shè)置為is_ref
                    Z_SET_ISREF_PP(array_ptr_ptr);
                }
            }
            array_ptr = *array_ptr_ptr;
            Z_ADDREF_P(array_ptr);
        }
    } else {
        ……
    }
    ……
}


問題2中已經(jīng)分析了一部分FE_RESET的實現(xiàn)。這里需要特別注意,本例foreach獲取值采用了引用,因此在執(zhí)行的時候FE_RESET中會進入與上題不同的另一個分支。
最終,F(xiàn)E_RESET會將array的is_ref設(shè)置為true,此時內(nèi)存中只有一份array的數(shù)據(jù)。
接下來分析SEND_REF:

復(fù)制代碼 代碼如下:


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已經(jīng)被設(shè)置了is_ref=true,因此它不會被拷貝一份副本。換句話說,此時內(nèi)存中依然只有一份array數(shù)據(jù)。

怎么在php中的foreach問題進行處理

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

復(fù)制代碼 代碼如下:


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;
}


由于此時內(nèi)部指針已經(jīng)指向了數(shù)組的最后一個元素,因此再向前移動會指向NULL。將內(nèi)部指針指向NULL之后,我們再對數(shù)組調(diào)用key和current,則分別會返回NULL和false,表示調(diào)用失敗,此時是echo不出字符的。
 問題4:

復(fù)制代碼 代碼如下:


$arr = array(1, 2, 3);
$tmp = $arr;
foreach($tmp as $k => &$v){
    $v *= 2;
}
var_dump($arr, $tmp); // 打印什么?


該題與foreach關(guān)系不大,不過既然涉及到了foreach,就一起拿來討論吧:)
代碼里首先創(chuàng)建了數(shù)組$arr,隨后將該數(shù)組賦給了$tmp,在接下來的foreach循環(huán)中,對$v進行修改會作用于數(shù)組$tmp上,但是卻并不作用到$arr。
為什么呢?
這是由于在php中,賦值運算是將一個變量的值拷貝到另一個變量中,因此修改其中一個,并不會影響到另一個。
題外話:這并不適用于object類型,從PHP5起,對象的便總是默認(rèn)通過引用進行賦值,舉例來說:

復(fù)制代碼 代碼如下:


class A{
    public $foo = 1;
}
$a1 = $a2 = new A;
$a1->foo=100;
echo $a2->foo; // 輸出100,$a1與$a2其實為同一個對象的引用


回到題目中的代碼,現(xiàn)在我們可以確定$tmp=$arr其實是值拷貝,整個$arr數(shù)組會被再復(fù)制一份給$tmp。理論上講,賦值語句執(zhí)行完畢之后,內(nèi)存中會有2份一樣的數(shù)組。
也許有同學(xué)會疑問,如果數(shù)組很大,豈不是這種操作會很慢?
幸好php有更聰明的處理辦法。實際上,當(dāng)$tmp=$arr執(zhí)行之后,內(nèi)存中依然只有一份array。查看php源碼中的zend_assign_to_variable實現(xiàn)(摘自php5.3.26):

復(fù)制代碼 代碼如下:


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數(shù)據(jù)的指針,variable_ptr_ptr為$tmp里指向數(shù)據(jù)指針的指針
                    // 僅僅是復(fù)制指針,并沒有真正拷貝實際的數(shù)組
                    *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的本質(zhì)就是將array的指針進行復(fù)制,然后將array的refcount自動加1.用圖表達(dá)出此時的內(nèi)存,依然只有一份array數(shù)組:
怎么在php中的foreach問題進行處理
既然只有一份array,那foreach循環(huán)中修改$tmp的時候,為何$arr沒有跟著改變?
繼續(xù)看PHP源碼中的ZEND_FE_RESET_SPEC_CV_HANDLER函數(shù),這是一個OPCODE HANDLER,它對應(yīng)的OPCODE為FE_RESET。該函數(shù)負(fù)責(zé)在foreach開始之前,將數(shù)組的內(nèi)部指針指向其第一個元素。

復(fù)制代碼 代碼如下:


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
                // 它會重新復(fù)制一個數(shù)組出來
                // 真正分離$tmp和$arr,變成了內(nèi)存中的2個數(shù)組
                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 {
        ……
    }

    // 重置數(shù)組內(nèi)部指針
    ……
}


從代碼中可以看出,真正執(zhí)行變量分離并不是在賦值語句執(zhí)行的時候,而是推遲到了使用變量的時候,這也是Copy On Write機制在PHP中的實現(xiàn)。
FE_RESET之后,內(nèi)存的變化如下:
怎么在php中的foreach問題進行處理


以上就是怎么在php中的foreach問題進行處理,小編相信有部分知識點可能是我們?nèi)粘9ぷ鲿姷交蛴玫降?。希望你能通過這篇文章學(xué)到更多知識。更多詳情敬請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI