内容简介:在相当长的一段时间里,我认为这个例子在循环体中修改数组不影响循环过程,副本的说法说得通。对于不同的PHP版本输出会有差异,php7 提及 foreach 的改变有三点:
起步
在相当长的一段时间里,我认为 foreach
在循环期间使用的是源数组的副本。但最近经过一些实验后发现这并不是百分百正确。
$array = array(1, 2, 3, 4, 5); foreach ($array as $item) { echo "$item\n"; $array[] = $item; } print_r($array); /* Output in loop: 1 2 3 4 5 $array after loop: 1 2 3 4 5 1 2 3 4 5 */
这个例子在循环体中修改数组不影响循环过程,副本的说法说得通。
$arr = [1, 2, 3, 4, 5]; $obj = [6, 7, 8, 9, 10]; $ref = &$arr; foreach ($ref as $val) { echo $val; if ($val == 3) { $ref = $obj; } } // output in php5.x: 123678910 // output in php7.x: 12345
对于不同的 PHP 版本输出会有差异,php7 提及 foreach 的改变有三点:
- foreach 不再改变内部数组指针;
- foreach 通过值遍历时,操作的值为数组的副本;
- foreach 通过引用遍历时,有更好的迭代特性。
因此,在讨论 foreach
里的数组副本问题,得分开版本来说明。在此, https://stackoverflow.com/questions/10057671/how-does-php-foreach-actually-work 有了比较详细的说明,并举例了大多数情况。本文就进行一些整理。
写时复制
造成运行差异和与预期不同的原因一部分就是因为触发了写时复制,另一部分是 foreach 本身的机制。
php底层有两个属性来处理引用计数(refcount)与完全引用计数(is_ref)。
当类似 $a = [1, 2, 3];
创建并初始化后,该对象 is_ref
会设为 0, refcount
会设为 1; 当类似 $b = &$a
时,is_ref 和 refcount 都会 +1 ; 当类似 $c = $a
时,refcount 会 +1。
什么情况下可以跳过写时复制而可以直接对源数组进行操作呢?
如果时候通过引用来进行迭代 foreach ($arr as $v)
,那么可以在迭代期间进行修改:
<?php $arr = [0, 1, 2, 3, 4, 5]; foreach ($arr as &$v) { if ($v === 0) { unset($arr[3]); } echo $v; } // output in 5.x: 01245 // output in 7.x: 01245
如果源数组事先被引用了 $ref = &$arr; foreach($arr as $v)
,对于在 5.x 下依然可以进行修改:
<?php $arr = [0, 1, 2, 3, 4, 5]; $ref = &$arr; foreach ($arr as $v) { if ($v === 0) { unset($arr[3]); } echo $v; } // output in 5.x: 01245 // output in 7.x: 012345
数组副本
数组内部指针(IAP)我们可以通过 current
观察它的移动,因为修改IAP也是在写时复制的语义下进行了。这也就意味着大多数情况下, foreach
都会被迫拷贝它正在迭代的数组。写时复制条件是操作对象的计数为 isref = 0
且 refcount > 1
。
foreach 对 current
的影响 7.x 的foreach已经不会修改内部指针了,所以讨论 current 影响的这部分都指 5.x 版本。
current 的例子1
<?php $arr = [0, 1, 2, 3, 4, 5]; foreach ($arr as $v) { echo current($arr); } // output in 5.x: 11111
这里有两个问题,一个是为什么第一次循环时 current 指向是第二个元素;另一个问题就是为什么都是指向第二个元素。
先来解释第一个问题,为什么第一次循环时 current 指向是第二个元素?
在 foreach
启动前,此时 $arr (is_ref=0, refcount=1),达不到写时复制的条件,因此用的是$arr本身。 这里有个细节,循环遍历某个数据结构的“正常”方式常常看起来像这样:
reset(arr); while (get_current_data(arr, &data) == SUCCESS) { code(); move_forward(arr); }
而 PHP 的 foreach
做的事情有些不同:
reset(arr); while (get_current_data(arr, &data) == SUCCESS) { move_forward(arr); code(); }
也就是说,在执行 foreach 的循环体之前,数组指针已经向前移动了。这意味着当循环体正在处理 $i
是,IAP 已经处于元素 $i+1
了。这也就是为什么第一次循环 current
得到的是第二个元素了。
那么,为什么下一个循环里 current 还是第二个元素呢?
这是因为底层会在 foreach 启动后对 refcount 进行 +1 ,因此在第一次循环后,foreach 准备修改内部指针了,但此时 $arr 为 is_ref=0 refcount=2,修改内部指针又在写时复制的语义下,因此触发了写时复制,所以从第二次循环开始,底层用的都是另外的一份副本,不再对原数组进行修改,所以 current($arr)
就一直停留在第二个元素上了。
current 的例子2
<?php $arr = [0, 1, 2, 3, 4, 5]; foreach ($arr as &$v) { echo current($arr); } // output in 5.x: 12345
这是foreach的运行机制导致的,只要是用引用进行迭代,foreach 操作的始终是原数组。这规则在 7.x 版本也适用。
current 的例子3
<?php $arr = [0, 1, 2, 3, 4, 5]; $foo = $arr; foreach ($arr as $v) { echo current($arr); } // output in 5.x: 000000
这个比 例子 1 就是多个一个将数组分配给另一个变量。这里,循环启动时 refcount=2,并且内部数组指针的移动又发生在循环体之前,所以一开始就触发了写时复制,foreach 始终都是在副本上操作。因此 current($arr)
总还是指向第一个元素。
关于 foreach 对 current 的影响鸟哥似乎有分享:
说是在Think 2015 PHP技术峰会,但我没找到视频,十分遗憾。
在迭代过程中修改原数组
为了确保我们对数组的修改能够事实生效,我们就要避免写时复制的情况,让foreach始终都操作原数组。这里最方便的就是用引用来迭代:
$array = array(1, 2, 3, 4, 5); foreach ($array as &$v1) { foreach ($array as &$v2) { if ($v1 == 1 && $v2 == 1) { unset($array[1]); } echo "($v1, $v2)\n"; } } // output in 5.x: (1, 1) (1, 3) (1, 4) (1, 5) // output in 7.x: (1, 1) (1, 3) (1, 4) (1, 5) // (3, 1) (3, 3) (3, 4) (3, 5) // (4, 1) (4, 3) (4, 4) (4, 5) // (5, 1) (5, 3) (5, 4) (5, 5)
此处的 (1, 2)
是缺少的部分,因为元素 1
已经被删除。但对于删除后的处理,5和7不同,5 在外循环第一次迭代后就中止了,这是因此 5.x 的循环中,当前的IAP位置会被备份到 HashPointer
中,循环体结束后当且仅当元素仍然存在时进行恢复,否则使用当前的IAP位置。而7.x的两个循环都具有完全独立的散列表迭代器,不再通过共享IAP进行交叉污染。
$array = [1, 2, 3, 4, 5]; $ref = &$array; foreach ($array as $val) { var_dump($val); $array[2] = 0; } /* output in 5.x: 1, 2, 0, 4, 5 */ /* output in 7.x: 1, 2, 3, 4, 5 */
对于 5.x 版本,原数组有被引用,因此不会触发写时复制,foreach 操作始终是原数组。
对于 7.x 版本,foreach 通过值遍历时,操作的都是数组的副本,这点在升级文档有提及。
现在有一个比较奇怪的边缘问题:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3]; foreach ($array as &$value) { unset($array['EzFY']); $array['FYFY'] = 4; var_dump($value); } // output in 5.x: 1, 4 // output in 7.x: 1, 3, 4
在5.x版本中,由于 HashPointer恢复机制会直接跳到新元素(这应该算是bug)。而版本 7.x 不再依赖元素哈希,因为它的运行结果更为正确。
在循环期间替换迭代的实体
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; } } /* Output: 1 2 3 6 7 8 9 10 */
额外
内部指针与 HashPointer
为了引出指针恢复的概念,我们可以能要从一个问题来入手,只有一个内部数组指针的要怎么同时满足两个循环:
// Using by-ref iteration here to make sure that it's really // the same array in both loops and not a copy foreach ($arr as &$v1) { foreach ($arr as &$v) { // ... } }
解决的办法是,在循环体执行之前,将当前元素的指针和指向的元素保存起来,在循环体运行后,如果元素仍然存在,就把IAP恢复为之前保存的指针;如果元素已被杀出,则IAP就使用当前的位置。这个保存的指针和元素地方就是 HashPointer
。
HashPointer
备份恢复机制带来的方便就是我们可以临时修改数组的指针:
$array = [1, 2, 3, 4, 5]; foreach ($array as &$value) { var_dump($value); reset($array); } // output: 1, 2, 3, 4, 5
如果要干涉这个机制,就要让他恢复失败:
$array = [1, 2, 3, 4, 5]; $ref =& $array; foreach ($array as $value) { var_dump($value); unset($array[1]); reset($array); } // output in 5.x: 1, 1, 3, 4, 5
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 理清楚Vue的结构
- 理清 Block 底层结构及其捕获行为
- 理清代码提交记录:代码管理的 git
- 教你理清SpringBoot与SpringMVC的关系
- 理清脉络——产品开发前都在做什么?
- 一文理清集成学习知识点(Boosting&Bagging)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据驱动设计
[美]罗谢尔·肯(RochelleKing)、[美]伊丽莎白F.邱吉尔(Elizabeth F Churchill)、Caitlin Tan / 傅婕 / 机械工业出版社 / 2018-8 / 69.00元
本书旨在帮你了解数据引导设计的基本原则,了解数据与设计流程整合的价值,避免常见的陷阱与误区。本书重点关注定量实验与A/B测试,因为我们发现,数据分析与设计实践在此鲜有交集,但相对的潜在价值与机会缺大。本书提供了一些关于在组织中开展数据实践的观点。通过阅读这本书,你将转变你的团队的工作方式,从数据中获得大收益。后希望你可以在衡量指标的选择、佳展示方式与展示时机、测试以及设计意图增强方面,自信地表达自......一起来看看 《数据驱动设计》 这本书的介绍吧!