php中each遍历,PHP foreach 是如何遍历数组的?

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,输出结果可想而知:

6f0de5b9d3d2f5e447ab0d913b90f1b8.png

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了:

48cdecb6f5717dd08884225aba0a5fcc.png

从输出结果图中,可以看到最后的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));

?>

输出结果:

b346befee85e3b28a51b1356e9912018.png

对于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的基础上,将赋值改为引用,看看输出结果:

d5904d39004318cc2e5b2a916b75219e.png

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));

}

?>

输出结果:

41f0d51f1f93813eff68c7475694f774.png

为什么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的输出结果你也应该能分析出来了:

45abc352ac909ee60bbee9147d2de48a.png

总结

如果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了,可结果如下:

6399065f2825d4ff33af2ce26ffca260.png

从输出结果可以看出,(zval).value->ht->arBuckets也是会被复制的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值