Array类型的实现
在PHP的zvalue_value结构体中,我们知道array类型是通过HashTable实现的,结构如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17typedef 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
?>
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也是会被复制的。