您现在的位置: 万盛学电脑网 >> 程序编程 >> 网络编程 >> php编程 >> 正文

深入解析php中的foreach问题

作者:佚名    责任编辑:admin    更新时间:2022-06-22

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:

 

01.png

我们从第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缓存了起来。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->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指令执行完毕之后,内存中实际情况如下:

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->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)所做的事情。它会根据ze