foreach虽然简单

文章作者:w88手机版登录网络 上传时间:2019-08-15

前言: php4中引进了foreach结构,那是一种遍历数组的简练方法。比较守旧的for循环,foreach能够越发方便的拿走键值对。在php5之前,foreach仅能用于数组;php5之后,利用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:图片 1

我们从第3行的ASSIGN指令看起,它象征将array('a','b','c')赋值给$arr。
鉴于$arr为CV,array('a','b','c')为TMP,因而ASSIGN指令找到实际实施的函数为ZEND_ASSIGN_SPEC_CV_TMP_HANDLE揽胜极光。这里需求非常建议,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缓存了起来。图片 2

接下去推行数组的巡回操作,咱们来看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指令实践落成之后,内部存款和储蓄器中实际情形如下:

图片 3

接下去我们承继查看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的指针去赢得数组成分,在赢得成功之后,将该指针移动到下二个职位再重复保存。

图片 4

简轻便单来讲,由于第贰回循环中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')复制了一份。因而变量分离之后的内部存储器为:图片 5
留神,变量分离完结现在,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 {
        ……
    }
    ……
}

难题第22中学已经剖判了一有个别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数据。

图片 6

上海体育场地解释了前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数组:
图片 7
既是唯有一份array,那foreach循环中期维修改$tmp的时候,为啥$arr未有随着变动?
后续看PHP源码中的ZEND_FE_RESET_SPEC_CV_HANDLEWrangler函数,那是多少个OPCODE HANDLE福特Explorer,它对应的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之后,内部存款和储蓄器的浮动如下:
图片 8

上海教室解释了为啥foreach并不会对原先的$arr发生震慑。至于ref_count以及is_ref的生成景况,感兴趣的校友能够详细阅读ZEND_FE_RESET_SPEC_CV_HANDLER和ZEND_SWITCH_FREE_SPEC_VAR_HANDLERubicon的切实落到实处(均位居php-src/zend/zend_vm_execute.h中),本文不做详细剖析:)

你只怕感兴趣的稿子:

  • php in_array 函数使用表明与in_array需求专注的地方表明
  • PHP 数组遍历方法大全(foreach,list,each)
  • php中用foreach来操作数组的代码
  • php数组函数连串之in_array() 查找数组值是不是存在
  • php循环语句 for()与foreach()用法分别介绍
  • php数组查找函数in_array()、array_search()、array_key_exists()使用实例
  • php下foreach提醒Warning:Invalid argument supplied for foreach()的缓慢解决方法
  • php中使用in_array() foreach array_search() 查找数组是还是不是含有的时候的质量相比
  • 浅谈PHP中foreach/in_array的使用

本文由w88手机版登录-www.w88zhan.com-优德w88app官方登录发布于w88手机版登录网络,转载请注明出处:foreach虽然简单

关键词: www.优德88.com