让我们跟上一章节一样,以一段神奇的PHP代码开始。
<?php
/** author:[email protected] */
/*** for_1.php */
$array=array("lucy"=>1983,"lilei"=>1982,"hanmeimei"=>1981);
foreach($array as $k=>$v){
print current($array)."\n";
}
运行一下:
$php for_1.php
1982
1982
1982
很奇怪,程序一直输出着第二项的值。现在我们有两个头大的问题:
- 为什么程序每次输出的是相同的值?
- 为什么程序每次都是输出的第二项的值?
为了解决这两个问题,我们先复习一下相关的数据结构和函数。
###foreach php官网对foreach的说明如下: http://cn2.php.net/manual/zh/control-structures.foreach.php foreach 语法结构提供了遍历数组的简单方式:
foreach (array_expression as $value)
statement
foreach (array_expression as $key => $value)
statement
- 第一种格式遍历给定的 array_expression 数组。每次循环中,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。
- 第二种格式做同样的事,只除了当前单元的键名也会在每次循环中被赋给变量 $key。 另外官网还有一段小说明,也是值得注意的:
Note:
当 foreach 开始执行时,数组内部的指针会自动指向第一个单元。这意味着不需要在 foreach 循环之前调用 reset()。
由于 foreach 依赖内部数组指针,在循环中修改其值将可能导致意外的行为。
这一段其实很重,揭示了foreach与while循环极大的不同。后面会讲到.
###current 再来复习一下current在官网的说明:
current — 返回数组中的当前单元
mixed current ( array &$array )
每个数组中都有一个内部的指针指向它“当前的”单元,初始指向插入到数组中的第一个单元。
读完这个说明,我们知道了,数组都有一个内部指针,标记着该数组当前的位置。正是因为如此,我们才可能 完成foreach这个语法,才能使用next() end()等函数。
下面我们使用vld工具来查看一下刚才的for_1.php的opcode:
$php -dvld.active=1 ./for_1.php
function name: (null)
number of ops: 22
compiled vars: !0 = $array, !1 = $k, !2 = $v
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > EXT_STMT
1 INIT_ARRAY ~0 1983, 'lucy'
2 ADD_ARRAY_ELEMENT ~0 1982, 'lilei'
3 ADD_ARRAY_ELEMENT ~0 1981, 'hanmeimei'
4 ASSIGN !0, ~0
3 5 EXT_STMT
6 > FE_RESET $2 !0, ->20
7 > > FE_FETCH $3 $2, ->20
8 > ZEND_OP_DATA ~5
9 ASSIGN !2, $3
10 ASSIGN !1, ~5
4 11 EXT_STMT
12 EXT_FCALL_BEGIN
13 SEND_REF !0
14 DO_FCALL 1 $7 'current'
15 EXT_FCALL_END
16 CONCAT ~8 $7, '%0A'
17 PRINT ~9 ~8
18 FREE ~9
5 19 > JMP ->7
20 > SWITCH_FREE $2
6 21 > RETURN 1
branch: # 0; line: 2- 3; sop: 0; eop: 6; out1: 7; out2: 20
branch: # 7; line: 3- 3; sop: 7; eop: 7; out1: 8; out2: 20
branch: # 8; line: 3- 5; sop: 8; eop: 19; out1: 7
branch: # 20; line: 5- 6; sop: 20; eop: 21
path #1: 0, 7, 8, 7, 20,
path #2: 0, 7, 20,
path #3: 0, 20,
1982
1982
1982
我们可以理清一下基本的结构,opcode 的#0 到#4是在进行数组的初始化[INIT_ARRAY],#6 reset了数组[FE_RESET], #7至#19之间是循环体,运行到#19后就跳转到了#7,而在#7如果取数据失败,就会跳到#20[SWITCH_FREE]。 而#20是一个释放资源的语句, 整个运行时是这样的轨迹: 先对数组做RESET,如果失败,跳到循环结束后一行。RESET之后,产生了一个临时变量$2。 以下为循环体:
- 从数组的拷贝变量$2中取一行数据;如果取数据失败,跳到循环结束后的一行;
- 上一步这个"取一行数据"的过程,就顺带将数组$2的指针移向了下一位.
- 执行相关的循环体操作。在循环体中,调用current时,传入的参数是$array,在OPcode中是$0 (不是$2哦。$2是foreach开始时,生成的临时变量); 循环结束时,调用了SWITCH_FREE,传的参数是$2,就是刚才foreach运行之初生成的临时变量。
###在执行foreach时,C语言层的代码流程:
foreach(){}的编译 oreach编译调用了zend_do_foreach_begin(), ($array as $k=>$v) 编译调用了zend_do_foreach_cont() foreach大括号结束时的编译调用的是zend_do_foreach_end();
我们重点看一下ZEND_FE_FETCH 这个OPCDE里的代码流程,并把跟array类型数据的FE_FETCH代码拉出来:
case ZEND_ITER_PLAIN_ARRAY: //zend_vm_execute.h 11719行php 5.4.18
fe_ht = Z_ARRVAL_P(array); //
zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.var).fe.fe_pos);
if (zend_hash_get_current_data(fe_ht, (void **) &value)==FAILURE) {
/* reached end of iteration */
ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.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);
zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.var).fe.fe_pos);
break;
为了方便理解,我们把zend_hash_set_pointer和zend_hash_get_pointer,zend_hash_move_forward的代码也展开了:
ZEND_API int zend_hash_set_pointer(HashTable *ht, const HashPointer *ptr)
{
if (ptr->pos == NULL) {
ht->pInternalPointer = NULL;
} else if (ht->pInternalPointer != ptr->pos) {
Bucket *p;
IS_CONSISTENT(ht);
p = ht->arBuckets[ptr->h & ht->nTableMask];
while (p != NULL) {
if (p == ptr->pos) {
ht->pInternalPointer = p;
return 1;
}
p = p->pNext;
}
return 0;
}
return 1;
}
ZEND_API int zend_hash_get_pointer(const HashTable *ht, HashPointer *ptr)
{
ptr->pos = ht->pInternalPointer;
if (ht->pInternalPointer) {
ptr->h = ht->pInternalPointer->h;
return 1;
} else {
ptr->h = 0;
return 0;
}
}
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;
}
zend_hash_set_pointer的代码可以简单理解为,其核心是,如果HashTable的内部指针位置 跟HashPosition的位置不一致,就把HashTable的内部指针指向HashPosition; zend_hash_get_pointer的代码可以简单理解为,HashPosition的指针指向HashTable的 内部指针的位置。(完整的逻辑是:如果当前HashTable的内部指针非空,则把HashPosition的指针指向HashTable的 内部指针的位置;如果HashTable的内部指针为空,则把opcode.fe.pos的指针置空).
hashTable的内部指针:HashTable这个结构体 定义了一个pInternalPointer成员变量,这个变量 指向当前的成员。只有有了个这个成员变量,才能做数组的next(),prev(),end(),reset()等操作。reset() 操作就是将当前数组的HashTable的pInternalPointer 指向链表头部.
现在我们简单整理了下FE_FETCH的逻辑:
- 取当前的数组的HashTable;
- 如果当前HashTable的内部指针跟opcode.fe.pos 记录的指针不一样,就把HashTable的指针指向这一个;
- 取出当前位置的数据,如果失败跳出循环;
- 如果数组的key也被用到,当前的key也存一下;
- 调用zend_hash_move_forward,将HashTable的内部指针前移一位;
- 将内部指针的位置复制给opcode.fe.pos。
现在我们清楚了,在foreach每次循环时,调用的是FE_FETCH 这个opcode,这个操作实际上将当前的 HashTable的指针是跟opcode.fe.pos绑定了,即便用户在foreach的代码体中间修改当前的内部指针,也是 不管用的,因为接下来再一次循环时,数组的内部指针又被修改到和opcode.fe.pos一致了。 来段PHP代码跑一下,让我们更明确一些:
<?php
$array=array("lucy"=>1983,"lilei"=>1982,"hanmeimei"=>1981,"lintao"=>1980);
$b = &$array;
foreach($array as $k=>$v){
print "{\n";
print "\tcurrent:".current($array)."\n";
print "\tnext:".next($array)."\n";
print "\tcurrent:".current($array)."\n";
print "\tnext:".next($array)."\n";
print "\tcurrent:".current($array)."\n";
print "}\n\n";
}
我们在循环体里调用了两次next。现在运行他:
{
current:1982
next:1981
current:1981
next:1980
current:1980
}
{
current:1981
next:1980
current:1980
next:
current:
}
{
current:1980
next:
current:
next:
current:
}
{
current:
next:
current:
next:
current:
}
观察一下,第一次循环时,我们的数组已经指到了1980这一项了,到下一次循环时,却又回到了1981 了。
再看另一段代码:
/** for_3.php */
<?php
$arr = array(0,1,2,3,4,5);
reset($arr);
//$b = & $arr;
//while (list($key,$v) = each($arr))
foreach($arr as $v){
if($v == 3){//改成v==2 v==4,结果又不一样;
$arr[6] = 6;
}
}
echo current($arr);
输出的结果4. 如果把中间那个表达式改为if($v==4) {$arr[6]=6;} ,结果又变成了5.
为什么呢?
这又涉及到另一个概念,写时复制.PHP内核为了节省内存,在涉及新变量时,默认不创建拷贝.只有 当这个变量的值发生改变时,才会真正的新建一个变量,将变量的值复制一份出来,修改新的变量值. 在刚才这段代码里,执行到$arr[6]=6时,php发现$arr这个变量产生了修改,因此将arr指向的变量 拷贝了一份,将$arr指向了这个新的变量.虽然就在前面我们刚刚提过,不论你在循环体里如何修改$arr 当前的内部指针了,在下一次循环时,PHP 执行for 循环时,都会将$arr的内部指针重置;但是, 由于写时复制,$arr 发生值修改时,$arr实际创建了另一个值并指向了新的值;因此,foreach虽然 执行了同步修内部指针的动作,但是却是修改了老的$arr变量的内部指针;新产生的$arr变量的内部指针一 直未被修改,因此最后调用current($arr),输出的就是$arr变量指向新变量时,内部指针的位置;
看看这段毁三观的代码:
<?php
/** while_1.php */
$mates = array("lucy","lilei","hanmeimei");
while($mate = each($mates)){
echo $mate["value"]."\n";
$another = $mates;//这一行代码导致了死循环;
}
以绝大多数PHP程序员的常识,这段代码就是简单的输出lucy,lilei,hanmeimei三个元素,就完事了;但是, 实际情况上,这个小脚本导致了疯狂的死循环. 经过查阅资料,找到了主要原因:把数据赋值给另一个变量时,会导致一个副作用:数组的内部指针会被重置(就跟调用了reset一样); 原文在:http://php.net/manual/zh/function.each.php
因为将一个数组赋值给另一个数组时会重置原来的数组指针,因此在上边的例子中如果我们在循环内部将 $fruit 赋给了另一个变量的话将会导致无限循环。
但是刚才我们刚刚了解过foreach的知识,foreach开始会重置数组的内部指针,每次循环时会同步指针,所以,foreach不会出现死循环的情况.我们可以用这个代码测试一下:
$mates = array("lucy","lilei","hanmeimei");
foreach($mates as $mate){
echo $mate."\n";
$another = $mates;
}
那么,使用while(each)的语句组合呢? 我们看一下使用while语句的while_1.php 的opcode:
compiled vars: !0 = $mates, !1 = $mate, !2 = $another
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > EXT_STMT
1 INIT_ARRAY ~0 'lucy'
2 ADD_ARRAY_ELEMENT ~0 'lilei'
3 ADD_ARRAY_ELEMENT ~0 'hanmeimei'
4 ASSIGN !0, ~0
3 5 EXT_STMT
6 > EXT_FCALL_BEGIN
7 SEND_REF !0
8 DO_FCALL 1 $2 'each'
9 EXT_FCALL_END
10 ASSIGN $3 !1, $2
11 > JMPZ $3, ->19
4 12 > EXT_STMT
13 FETCH_DIM_R $4 !1, 'value'
14 CONCAT ~5 $4, '%0A'
15 ECHO ~5
5 16 EXT_STMT
17 ASSIGN !2, !0
6 18 > JMP ->6
7 19 > > RETURN 1
branch: # 0; line: 2- 3; sop: 0; eop: 5; out1: 6
branch: # 6; line: 3- 3; sop: 6; eop: 11; out1: 12; out2: 19
branch: # 12; line: 4- 6; sop: 12; eop: 18; out1: 6
branch: # 19; line: 7- 7; sop: 19; eop: 19
path #1: 0, 6, 12, 6, 19,
path #2: 0, 6, 19,
我们会发现.....while没有引入特殊的OPCODE!和foreach语句的待遇大不相同啊;PHP引入了 FE_RESET,FE_FETCH来实现foreach,中间还悄悄地生成了一个临时变量;但是却只用了一个JMP,一个JUMPZ就搞定了while. 这就解释了,为什么foreach足够"聪明",不管怎么折腾,都不会多循环或是少循环一次,但是while(each)就不行了, 搞不好就死循环了,一切仅仅只是因为,foreach比while的待遇高,是专有实现.