PHP foreach 是如何遍历数组的?
参考资料:How foreach actually works和PHP的哈希表实现
Array类型的实现
在PHP的zvalue_value结构体中,我们知道array类型是通过HashTable实现的,结构如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _hashtable {
uint nTableSize; // hash Bucket的大小,最小为8,以2x增长。
uint nTableMask; // nTableSize-1 , 索引取值的优化
uint nNumOfElements; // hash Bucket中当前存在的元素个数,count()函数会直接返回此值
ulong nNextFreeElement; // 下一个数字索引的位置
Bucket *pInternalPointer; // 当前遍历的指针(foreach比for快的原因之一)
Bucket *pListHead; // 存储数组头元素指针
Bucket *pListTail; // 存储数组尾元素指针
Bucket **arBuckets; // 存储hash数组
dtor_func_t pDestructor; // 在删除元素时执行的回调函数,用于资源的释放
zend_bool persistent; //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
Bucket *pInternalPointer;
就是foreach
用于遍历Bucket
的指针,数组的每一个元素都存储在Bucket
,它比for
快,因为for
需要对key
进行哈希后,才能找到相应节点。
请看下面的示例代码以了解foreach对数组的遍历
Test Case 1:
1
2
3
4
5
6
7
<?php
$arr = [1, 2, 3, 4, 5];
foreach ($arr as $key => $value) {
echo "$value\n";
}
?>
上面的代码很简单,foreach
使用pInternalPointer
逐个遍历Bucket
,输出结果可想而知:
Test Case 2:
1
2
3
4
5
6
7
8
<?php
$arr = [1, 2, 3, 4, 5];
var_dump(each($arr));//先将pInternalPointer往前挪一位
foreach ($arr as $key => $value) {
echo "$value\n";
}
var_dump(current($arr));//输出当前pInternalPointer指向的Bucket
?>
根据Manual所说:
When foreach first starts executing, the internal array pointer is automatically reset to the first element of the array. This means that you do not need to call reset() before a foreach loop.
那么示例代码中的foreach
应该能正常输出1-5了:
从输出结果图中,可以看到最后的var_dump(current($arr));
输出结果为false,原因就在于foreach
发现$arr
的refcount__gc
为1,is_ref__gc
为0,此时foreach
并不会复制$arr
指向的zval,而是将refcount__gc
的值加1。当foreach在遍历元素时,使用的就是$arr的(zval).value->ht->pInternalPointer
,所以当遍历结束时,pInternalPointer
已经指向null了,因此var_dump(current($arr));
的输出结果为false。
Test Case 3:
1
2
3
4
5
6
7
8
<?php
$arr = [1, 2, 3, 4, 5];
$t = $arr;
foreach ($arr as $key => $value) {
echo "$value\n";
}
var_dump(current($arr));
?>
输出结果:
对于TC3的示例代码,var_dump(current($arr));
输出了1。为什么不是false呢?因为foreach发现$arr
的refcount__gc
为2,is_ref__gc
为0,也就是说有变量“引用”了$arr
,如果改变了$arr
的pInternalPointer
,那么$t
的pInternalPointer
也会被改变,为了不影响$t
,foreach
对$arr
指向的zval进行了复制之后再遍历。
Test Case 4:
1
2
3
4
5
6
7
8
<?php
$arr = [1, 2, 3, 4, 5];
$t = &$arr;
foreach ($arr as $key => $value) {
echo "$value\n";
}
var_dump(current($arr));
?>
在TC3的基础上,将赋值改为引用,看看输出结果:
var_dump(current($arr));
的输出又变回false了。这是因为foreach发现$arr
的is_ref__gc
为1,说明有其它变量引用了$arr
,这时,foreach就会将其和TC2的情况同等看待,并不会复制$arr
的zval。
Test Case 5:
1
2
3
4
5
6
7
<?php
$arr = [1, 2, 3, 4, 5];
foreach ($arr as $key => &$value) {
echo "$value\n";
}
var_dump(current($arr));
?>
TC5的输出结果将和TC4一样,根据Manual所说:
In order to be able to directly modify array elements within the loop precede $value with &.
所以这里的$arr
的is_ref__gc
为1,没有发生zval复制。
这样就结束了吗?还没有呢,继续看
Test Case 6:
1
2
3
4
5
6
7
<?php
$arr = [1, 2, 3, 4, 5];
foreach ($arr as $key => $value) {
echo "$value\n";
var_dump(current($arr));
}
?>
输出结果:
为什么var_dump(current($arr));
的输出都是2呢?foreach
应该没有复制$arr
呀?
首先解释为什么都是2,因为foreach其实是这样运行的,看下面伪代码:
1
2
3
4
5
6
7
<?php
reset();
while (get_current_data(&data) == SUCCESS) {
move_forward();
code();//var_dump(current($arr));在这里执行
}
?>
1、取值2、指针往前移3、执行用户代码。
对于第二个问题,foreach
并没有复制$arr
,关键在于current()
函数的调用,该函数是通过引用传递的,current()
发现$arr
的refcount__gc
为2,is_ref__gc
为0,所以就进行了zval的分离,复制了一份zval,$arr
就指向了这分新的zval,而foreach仍然使用“旧”的zval在遍历。
Test Case 7:
1
2
3
4
5
6
7
8
9
10
11
12
<?php
$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];
$ref =& $arr;
foreach ($ref as $val) {
echo "$val\n";
if ($val == 3) {
$ref = $obj;
}
}
?>
如果已经理解了上面6个示例,那么TC7的输出结果你也应该能分析出来了:
总结
如果foreach
要遍历一个数组:
- 数组的
refcount__gc
为1,is_ref__gc
为0,那么foreach
并不会复制zval; - 数组的
refcount__gc
>1,is_ref__gc
为0,那么foreach
将会复制zval; - 数组的
is_ref__gc
为1,那么foreach
并不会复制zval; - 注意在遍历的时候也会发生数组zval的复制,如TC6。
附加的一个问题
有人问道当foreach发生zval复制时,从上面的例子可以得出这样的结论:(zval).value->ht
会被复制一份,那么(zval).value->ht->arBuckets
即该二级指针存储的Bucket
是否也会被复制?
看这段示例代码:
1
2
3
4
5
6
7
8
9
10
<?php
$arr = [1, 2, 3, 4, 5];
$ref = $arr;
foreach ($arr as $val) {
echo "$val\n";
$arr[] = $val + 1;
}
var_dump($arr);
?>
如果(zval).value->ht->arBuckets
没有被复制,那么foreach
的输出就不止1、2、3、4、5了,可结果如下:
从输出结果可以看出,(zval).value->ht->arBuckets
也是会被复制的。