PHP中基础中的三大坑,foreach遍历,引用机制&,数组。
今天我们在讲讲foreach中的一些奇怪现象。
在讲解之前,可以先看看我其他相关的文章,属于同一个大的知识点,都看看有助于理解。
当我们使用foreach时,内部究竟发生了什么?(PHP5)
PHP底层分析:关于写时复制(cow)
PHP底层分析:关于强制分裂
△△△写前声明:以下结论都基于PHP5版本,因为时代在进步,在PHP7中内部的结构体模块和引用模块均发生重大变化,PHP7的foreach输出规则也旋即发生变化。当然,由于PHP7想要普及还需要一到两年(今年是2016)时间,所以这篇文章还是有些价值的,至少可以让你先理解一下PHP内部实现。
有疑问可以直接在评论中抛出,我会一一解答。
本文适合有一定基础的PHPer。
那么开始上图,这是鸟哥(惠新宸,PHP7核心开发组成员,开发组中唯一的一个中国人[撒花])在Think 2015 PHP技术峰会上的一个演讲截图,他在讲述PHP5的foreach和PHP7的foreach区别,我们把他演讲中提及到PHP5的部分拿出来看看。
那么我们着重看下这幅图的三段代码执行流程。
我讲讲三段代码的运行原理
code1.php
<?php $a = array(1,2,3); foreach ($a as $key => $value) { var_dump(current($a)); //output int(2) int(2) int(2) } ?>
输出值为: int(2) int(2) int(2) 。
同学们可能纳闷了,乍一看并没有发生明显的写时复制(相关文章)或者强制分裂(相关文章),怎么会是三个'2'呢。
关键点在于current()函数上:
foreach循环开始,拷贝一个数组出来,然后refcount_gc=2(foreach原理不太了解的同学,可以看看我的另一篇文章: 当我们使用foreach时,内部究竟发生了什么?(PHP5) )
此时原数组($a)和拷贝数组(我这里命名为$a_copy)的指针均指向下标1,
随后进入大括号执行体,current()操作的参数必须是引用数组(红线部分),如果不是引用数组的话会强制转换成引用数组(即结构体中is_ref__gc从0 -> 1)。
根据强制分裂原理,一个结构体的is_ref__gc的值从0 -> 1的时候,如果refcount_gc=2时,就会发生url"强制分裂了"。
强制分裂后, 原数组($a)和拷贝数组(我这里命名为$a_copy)结构体已经不一样,但是foreach操作的是拷贝数组($a_copy),原数组被丢在半道上了,所以三次输出var_dump(current($a))均为2
code2.php
<?php $a = array(1,2,3); $b = &$a; // 结构体中:refcount_gc=2;is_ref_gc=1; foreach ($a as $value) { var_dump(current($a)); //output: int(2) int(3) bool(false) } ?>
这段代码和code1.php原理差不多,只是在foreach前进行了一次引用赋值,结构体变化成:refcount_gc=2;is_ref_gc=1; 随后foreach遍历数组,此时原数组($a),拷贝数组(我这里命名为$a_copy)和$b的指针均指向下标1并且均为引用数负责(is_ref_gc)。
接着进入大括号执行体:var_dump(current($a)); 前面说道",current()操作的参数必须是引用数组(红线部分),如果不是引用数组的话会强制转换成引用数组(即结构体中is_ref__gc从0 -> 1)。"
此处参数$a已经为引用数组,不会发生强制分裂,原数组($a)和拷贝数组($a_copy)为同一个结构体,正常输入为:int(2) int(3) bool(false)
code3.php
<?php $a = array(1,2,3); $b = $a; // 结构体中:refcount_gc=2 foreach ($a as $key => $value) { var_dump(current($a)); //output int(1) int(1) int(1) } ?>
第三段代码和前面两个又有所不同了,这次$b=$a是使用传值赋值,那么此处为什么为3个int(1)呢?这次的原因在于foreach的机制:
foreach循环之前,需要判断数组的 refcount计数,如果大于1,拷贝数组自己成为一个新的结构体,循环过程操作的是新的结构体。
运行流程: line:3; // $a,$b共用一个结构体 line:4; //foreach开始遍历数组,进行refcount判断,拷贝数组是一个新的结构体,foreach操作的是新的结构体。那么遍历数组时,全程与原数组无关。 Line:5: //打印数组当前单元,由于原数组和拷贝数组已不是一个结构体,所以原数组的指针没变过,打印出int(1)。
如果还是有些不明觉厉的话,可以反复看多几遍,对了,再次推荐看下这几篇文章,都能理解的话对foreach掌握也差不多够用了。
当我们使用foreach时,内部究竟发生了什么?(PHP5)
PHP底层分析:关于写时复制(cow)
PHP底层分析:关于强制分裂