背景描述
先看一段代码。
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
foreach ($arr as &$val) {
echo $val;
}
foreach ($arr as $val) {
echo $val;
}
print_r($arr);
想一下应该输出什么呢?
运行一下脚本,真实结果和你想的是否一致呢?
在foreach中使用了引用后再次foreach发现$arr['less']的值变成了54,常规理解应该是23才对。
猜测可能是因为使用引用导致该值变为54 但本着知其然更要知其所以然 我们一起追一下php源码 是什么原因导致的
环境准备
工欲善其事必先利其器,先下载调试工具及源码
安装Visual Studio
下载Visual Studio 2017,并安装
下载地址:https://www.visualstudio.com/zh-hans/downloads/
下载php源码
因神马前端目前使用php7.0,因此下载php7.0的最新版 7.0.27为研究对象
下载地址:http://cn2.php.net/distributions/php-7.0.27.tar.bz2
创建解决方案
根据已有目录生成解决方案
创建成功后如下图所示
源码追踪
词法分析阶段
搜索关键字foreach
可以在zend_language_parser.c 中看到, 语法解析时 foreach会当做T_FOREACH
在zend_language_parser.y可以看到语法解析的具体方式
ZEND_AST_FOREACH
查找zend_ast_create
zend_ast.c中:
zend_ast_create 函数是创建一个抽象语法树(abstract syntax tree)返回的zend_ast结构如下:
具体的赋值操作如下:
编译生成opcode
接下来在zend_compile.c中根据抽象语法树生成opcode:
通过上图及语法解析的分析可知,foreach在编译阶段会生成如上图的四个zend_ast节点,分别表示:要遍历的数组或对象expr_ast,要遍历的value value_ast,要遍历的key key_ast,循环体stmt_ast
如:
$arr = [1, 2, 3];
foreach ($arr as $key => $val) {
echo $val;
}
expr_ast 是可理解为是$arr编译时对应的ast结构
value_ast对应$val
key_ast对应$key
stmt_ast对应"echo $val;"
copy一份要遍历的数组或对象,如果是引用则把原数组或对象设为引用类型
如:
foreach ($arr as $k => $v) {
echo $v;
}
copy一份$arr用于遍历,从arData的首元素起,把bucket.zval.value赋值给$v,把bucket.h或key赋值给$k,然后将下一个元素的位置记录在zval.u2.fe_iter_idx中,下次遍历从该位置开始,当u2.fe_iter_idex到了arData的末尾则遍历结束并销毁copy的$arr副本
如果$v是引用 则在循环前,将原$arr设置为引用类型 即:
foreach ($arr as $k => &$v) {
echo $v;
}
- 编译copy的数组、对象操作的指令:增加一条opcode指令 ZEND_FE_RESET_R(如果value是引用则用ZEND_FE_RESET_RW) 。执行时如果发现遍历的不是数组、对象 则抛出一个warning,然后跳出循环。
- 编译fetch数组、对象当前单元key 、value的opcode : ZEND_FE_FETCH_R(如果value是引用则用ZEND_FE_FETCH_RW)。此opcode需要知道当遍历到达数组末尾时跳出遍历的位置。此外还会对key和value分配他们在内存中的位置,如果value不是一CV个变量,还会编译其它操作的opcode
- 如果定义了key,则会编译一条opcode,对key进行赋值
- 编译循环体statement
- 编译跳回遍历开始时的opcode,一次遍历结束后跳到步骤2编译的opcode进行下次遍历
- 设置步骤1、2两条opcode如果出错要跳到的opcode
- 结束循环 编译ZEND_FE_FREE用于释放1中copy的数组或对象
结论分析
编译后的结构
运行时步骤:
- 执行ZEND_FE_RESET_R,过程上面已经介绍了;
- 执行ZEND_FE_FETCH_R,此opcode的操作主要有三个:检查遍历位置是否到达末尾、将数组元素的value赋值给$value、将数组元素的key赋值给一个临时变量(注意与value不同);
- 如果定义了key则执行ZEND_ASSIGN,将key的值从临时变量赋值给$key,否则跳到步骤(4);
- 执行循环体的statement;
- 执行ZEND_JMPNZ跳回步骤(2);
- 遍历结束后执行ZEND_FE_FREE释放数组。
根据上面的分析可知:赋值的核心操作是ZEND_FE_FETCH_R和ZEND_FE_FETCH_RW
等价关系
最开始举的例子可等价于
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
$val = &$arr['jack'];
$val = &$arr['tom'];
$val = &$arr['marry'];
$val = &$arr['less'];
$val = $arr['jack'];
$val = $arr['tom'];
$val = $arr['marry'];
$val = $arr['less'];
print_r($arr);
等价于:
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
$val = &$arr['less']; (23)
$val = $arr['marry']; (54,并且此时因为引用 $arr['less']也变为54了)
$val = $arr['less']; (54)
print_r($arr);
建议
因此 为了避免出现不必要的错误,建议在使用完&后,unset掉变量以取消对地址的引用
思维发散
针对以上情况,如果不取消对变量的引用,而是将数组赋值给一个新的变量再foreach。是否可行?
普通变量的引用
先看一段代码:
<?php
$str = '20';
$c = &$str;
$a = $str;
$c = 30;
var_dump($a);
输出20 没有任何问题
数组整体引用
如果换成数组:
<?php
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
$b = &$arr;
$a = $arr;
$b['jack'] = 30;
var_dump($a);
还是20 符合预期
数组元素引用
但如果这样呢:
<?php
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
$b = &$arr['jack'];
$a = $arr;
$b = 30;
var_dump($a)
值却变成了30
xdebug_debug_zval调试
我们加上xdebug_debug_zval看看发生了什么
<?
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
$b = &$arr;
$a = $arr;
$b['jack'] = 30;
var_dump($a);
xdebug_debug_zval('a');
xdebug_debug_zval('arr');
可以看出,直接引用数组, $b = &$arr, $arr 的is_ref是1,refcount是2, 给$a = $arr时,发生分离,$a 与$arr指向不同的zval,$b 与 $arr指向相同的zval,因此给$b['jack'] = 30, $a的值不会发生改变
<?
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
$b = &$arr['jack'];
$a = $arr;
$b = 30;
var_dump($a);
xdebug_debug_zval('a');
xdebug_debug_zval('arr')
可以看出,对数组中一个元素引用时,数组的is_ref是0,因为$a = $arr 因此refcount是2 ,指向同一个zval,改变$b的值时,因为$arr['jack']是一个引用,zval的值改变,$a和$arr的zval相同,$a['jack']也变为30
结论
同理可以回答最开始提出的疑问:如果我不取消对变量的引用,而是将数组赋值给一个新的变量再foreach。是否可行?答:不行。
<?
$arr = [
'jack' => '20',
'tom' => '21',
'marry' => '54',
'less' => '23'
];
foreach ($arr as &$val) {
echo $val;
}
$a = $arr;
foreach ($a as $val) {
echo $val;
}
print_r($a);
因为$arr与$a指向同一份zval,还是会出现$a['less'] = 54的结果。因此,在foreach使用完&后,还是unset掉变量 取消对地址的引用再进行下一步操作吧
参考文献:
[https://github.com/pangudashu/php7-internal/blob/master/4/loop.md
](https://github.com/pangudashu/php7-internal/blob/master/4/loop.md)
更多文章见:https://nc2era.com