前言:
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";
}
复制代码 代码如下:
foreach($arr as $k => $v){
//在用户代码执行之前隐含了2个赋值操作
$v = currentVal();
$k = currentKey();
//继续运行用户代码
……
}
复制代码 代码如下:
$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
复制代码 代码如下:
$arr = array('a','b','c');
foreach($arr as $k => $v) {
echo key($arr), "=>", current($arr);
}
// 打印 1=>b 1=>b 1=>b
复制代码 代码如下:
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();
}
复制代码 代码如下:
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 {
……
}
……
}
接下来我们继续查看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内部调用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();
}
复制代码 代码如下:
#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv) \
if (!PZVAL_IS_REF(*ppzv)) { \
SEPARATE_ZVAL(ppzv); \
Z_SET_ISREF_PP((ppzv)); \
}
复制代码 代码如下:
$arr = array('a','b','c');
foreach($arr as $k => &$v) {
echo key($arr), '=>', current($arr);
}// 打印 1=>b 2=>c =>
复制代码 代码如下:
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 {
……
}
……
}
复制代码 代码如下:
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();
}
上图解释了前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;
}
复制代码 代码如下:
$arr = array(1, 2, 3);
$tmp = $arr;
foreach($tmp as $k => &$v){
$v *= 2;
}
var_dump($arr, $tmp); // 打印什么?
复制代码 代码如下:
class A{
public $foo = 1;
}
$a1 = $a2 = new A;
$a1->foo=100;
echo $a2->foo; // 输出100,$a1与$a2其实为同一个对象的引用
复制代码 代码如下:
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;
}
复制代码 代码如下:
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 {
……
}
// 重置数组内部指针
……
}