在PHP中,foreach 语法结构提供了遍历数组的简单方式。 foreach 仅能够应用于数组和对象,如果尝试应用于其他数据类型的变量,或者未初始化的变量,将导致错误。 foreach每次循环时,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。
但是手册中提醒我们:
Note:
当 foreach 开始执行时,数组内部的指针会自动指向第一个单元。这意味着不需要在 foreach 循环之前调用 reset()。
在循环中修改 foreach 依赖其内部数组指针将可能导致意外的行为。 |
这里我们所要说的是foreach可能导致的意外情况。如代码1示例:
<?php
$arr = array(1,2,3,4,5);
foreach($arr as $key => &$row) {
echo key($arr), '=>', current($arr), "\r\n";
} |
会输出什么?
如代码2示例呢?
<?php
$arr = array(1,2,3,4,5);
foreach($arr as $key => $row) {
echo key($arr), '=>', current($arr), "\r\n";
} |
会输出什么?
代码1会依次输出变量,但是第一个元素并没有在输出结果中出现。
代码2只会输出数组的第二个元素。
为什么呢?
将代码2在VLD扩展中查看,
number of ops: 22
compiled vars: !0 = $arr, !1 = $key, !2 = $row
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > INIT_ARRAY ~0 1
1 ADD_ARRAY_ELEMENT ~0 2
2 ADD_ARRAY_ELEMENT ~0 3
3 ADD_ARRAY_ELEMENT ~0 4
4 ADD_ARRAY_ELEMENT ~0 5
5 ASSIGN !0, ~0
4 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
5 11 SEND_REF !0
12 DO_FCALL 1 $7 'key'
13 ECHO $7
14 ECHO '%3D%3E'
15 SEND_REF !0
16 DO_FCALL 1 $8 'current'
17 ECHO $8
18 ECHO '%0D%0A'
6 19 > JMP ->7
20 > SWITCH_FREE $2
8 21 > RETURN 1 |
从上面VLD扩展输出结果结合PHP的源代码可以知道,在foreach遍历之前, PHP内核首先会有个FE_RESET操作来重置数组的内部指针,也就是pInternalPointer, 然后通过每次FE_FETCH将pInternalPointer指向数组的下一个元素,从而实现顺序遍历。
并且每次FE_FETCH的结果都会被一个全局的中间变量存储,以给下一次的获取元素使用。
从这两个例子可以引申出三个问题:
1、为什么foreach循环体中执行key或current会显示第二个元素(非引用情况)?
以key函数为例,我们执行函数调用时,会执行中间代码SEND_REF,此中间代码会将没有设置引用的变量复制一份并设置为引用。当进入循环体时,PHP内核已经经过了一次fetch操作,相当于执行了一次next操作,当前元素指向第二个元素。因此我们在foreach的循环体中执行key函数时,key中调用的数组变量为PHP执行了一次fetch操作的数组拷贝,此时foreach的内部指针指向第二个元素。
2、为什么在foreach中执行end等操作,其循环过程不变?
在遍历的代码中通过end,next等操作数组的指针,数组的指针不会变化,这是因为在PHP内核进行FETCH操作时,会通过中间变量存储当前操作数组的内部指针,每遍历一个元素,会先获取之前存储的指针位置,获取下一个元素后,再恢复指针位置。
3、为什么$row的引用和非引用情况下输出结果不同?
如果是引用,PHP内核在reset数组时,会直接分裂数组,生成一个数组的拷贝,并将其设置为引用。
如果是非引用,PHP内核在reset数组时,当数组的引用计数大于1,并且不存在引用时,会拷贝数组供foreach使用,其它情况使用原数组,将其引用计数加1。
因为引用的不同,在循环体中给函数传递参数时其结果不同,导致看到的foreach数组内部指针变化的不同。对于非引用且引用计数大于1的情况,其本身就是两个不同的数组,在RESET时就不同了。