【从入门到放弃-PHP】foreach 引用的坑

简介: 背景描述 先看一段代码。 $arr = [ 'jack' => '20', 'tom' => '21', 'marry' => '54', 'less' => '23' ]; foreach ($arr as &$val) { echo $val; .

背景描述

先看一段代码。

$arr = [
    'jack'  => '20',
    'tom'   => '21',
    'marry' => '54',
    'less'  => '23'
];
foreach ($arr as &$val) {
    echo $val;
}
foreach ($arr as $val) {
    echo $val;
}
print_r($arr);

想一下应该输出什么呢?

运行一下脚本,真实结果和你想的是否一致呢?
image.png
在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

创建解决方案

根据已有目录生成解决方案

创建成功后如下图所示
image.png

源码追踪

词法分析阶段

搜索关键字foreach
image.png

可以在zend_language_parser.c 中看到, 语法解析时 foreach会当做T_FOREACH
image.png

在zend_language_parser.y可以看到语法解析的具体方式
image.png

ZEND_AST_FOREACH
image.png
image.png

查找zend_ast_create
image.png

zend_ast.c中:
image.png

zend_ast_create 函数是创建一个抽象语法树(abstract syntax tree)返回的zend_ast结构如下:
image.png

具体的赋值操作如下:
image.png

编译生成opcode

接下来在zend_compile.c中根据抽象语法树生成opcode:
image.png

通过上图及语法解析的分析可知,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;"

image.png

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副本

image.png

如果$v是引用 则在循环前,将原$arr设置为引用类型 即:
foreach ($arr as $k => &$v) {
    echo $v;
}

image.png

image.png

  • 编译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的数组或对象

结论分析

编译后的结构

image.png
运行时步骤:

  • 执行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);

image.png

建议

因此 为了避免出现不必要的错误,建议在使用完&后,unset掉变量以取消对地址的引用

思维发散

针对以上情况,如果不取消对变量的引用,而是将数组赋值给一个新的变量再foreach。是否可行?

普通变量的引用

先看一段代码:

<?php
$str = '20';
$c = &$str;
$a = $str;
$c = 30;
var_dump($a);

image.png

输出20 没有任何问题

数组整体引用

如果换成数组:

<?php
$arr = [
    'jack'  => '20',
    'tom'   => '21',
    'marry' => '54',
    'less'  => '23'
];
$b = &$arr;
$a = $arr;
$b['jack'] = 30;
var_dump($a);

image.png
还是20 符合预期

数组元素引用

但如果这样呢:

<?php
$arr = [
    'jack'  => '20',
    'tom'   => '21',
    'marry' => '54',
    'less'  => '23'
];
$b = &$arr['jack'];
$a = $arr;
$b = 30;
var_dump($a)

image.png

值却变成了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');

image.png

可以看出,直接引用数组, $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')

image.png

可以看出,对数组中一个元素引用时,数组的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);

image.png

因为$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

目录
相关文章
|
3月前
|
安全 关系型数据库 MySQL
PHP与MySQL交互:从入门到实践
【9月更文挑战第20天】在数字时代的浪潮中,掌握PHP与MySQL的互动成为了开发动态网站和应用程序的关键。本文将通过简明的语言和实例,引导你理解PHP如何与MySQL数据库进行对话,开启你的编程之旅。我们将从连接数据库开始,逐步深入到执行查询、处理结果,以及应对常见的挑战。无论你是初学者还是希望提升技能的开发者,这篇文章都将为你提供实用的知识和技巧。让我们一起探索PHP与MySQL交互的世界,解锁数据的力量!
|
21天前
|
PHP
PHP中的面向对象编程入门
在PHP的海洋里,面向对象编程(OOP)是一艘承载着代码复用与组织之美的巨轮。本文将带你启航,从基础概念到实际应用,领略类与对象的风采,掌握封装、继承、多态三大奥义。准备好你的航海图,让我们揭开PHP OOP的神秘面纱,驶向高效编程的彼岸。
|
3月前
|
PHP 开发者
PHP中的面向对象编程:从入门到精通
【9月更文挑战第27天】在这篇文章中,我们将一起探索PHP中的面向对象编程(OOP)的美妙世界。我们将从基础的概念和语法开始,然后深入到更高级的主题,如继承、多态和封装。无论你是PHP新手还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。让我们一起踏上这段旅程,发现PHP OOP的强大功能吧!
44 8
|
1月前
|
Java 程序员 PHP
01 入门PHP就来我这-安装phpstudy
路老师的PHP入门教程,带你从零开始学习PHP。首先下载并安装phpStudy,接着配置域名和端口,最后创建并运行第一个PHP文件。内容详实,适合初学者。
47 3
01 入门PHP就来我这-安装phpstudy
|
1月前
|
安全 关系型数据库 PHP
探索PHP:从入门到精通
【10月更文挑战第38天】在这篇文章中,我们将一起踏上PHP的探索之旅。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。我们将从PHP的基础开始,逐步深入到更复杂的主题,包括面向对象编程、数据库操作、安全性问题等。最后,我们将通过一些实用的代码示例,来展示PHP的强大功能和灵活性。让我们一起开始这段旅程吧!
14 2
|
1月前
|
存储 Serverless PHP
PHP编程入门:从基础到实战
【10月更文挑战第35天】本文将带你走进PHP的世界,从最基本的语法开始,逐步深入到实际应用。我们将通过简单易懂的语言和实际代码示例,让你快速掌握PHP编程的基础知识。无论你是初学者还是有一定经验的开发者,都能在这篇文章中找到你需要的内容。让我们一起探索PHP的魅力吧!
|
2月前
|
PHP 开发者
PHP中的异常处理:从入门到精通####
本文将深入浅出地探讨PHP中的异常处理机制,包括异常的基本概念、如何抛出与捕获异常、自定义异常类以及最佳实践。无论你是PHP新手还是经验丰富的开发者,都能从中学到实用的知识,帮助你编写更健壮的代码。 --- ####
|
1月前
|
自然语言处理 关系型数据库 MySQL
PHP编程入门:构建你的第一个网页应用
【10月更文挑战第29天】本文旨在引导初学者步入PHP编程的世界,通过深入浅出的方式介绍PHP的基础知识,并指导读者如何动手实践,搭建一个简单的网页应用。文章不仅涉及PHP代码的编写,还包括了环境配置、项目结构设计以及前后端交互的基本概念。适合对Web开发感兴趣且希望快速入门的朋友阅读。
48 0
|
3月前
|
前端开发 PHP 数据库
PHP编程入门:从基础到实战
【9月更文挑战第23天】本文将引导你进入PHP编程的世界,从基础知识到实际项目应用,逐步深入。我们将一起探索PHP的基本语法、常用函数和框架使用,以及如何在实际项目中运用PHP进行开发。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供有价值的参考和启示。让我们一起踏上PHP编程的学习之旅吧!
|
3月前
|
SQL 安全 Java
PHP编程之美:从入门到精通
【9月更文挑战第18天】在这篇文章中,我们将一起探索PHP编程语言的魅力和力量。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。我们将从PHP的基本概念开始,然后深入到更复杂的主题,如面向对象编程、数据库交互和安全性等。最后,我们将通过一些实用的代码示例来巩固我们的知识。让我们一起开始这段旅程吧!
42 4