
最小化分析代码:
$data = ['foo', 'bar']; foreach ($data as &$item) { } foreach ($data as $item) { } prnt_r($data); 输出结果:
Array ( [0] => 'foo' [1] => 'foo' ) 我们可以发现,$data的值 ~~莫名奇妙~~ 变了,而它只是经过了两个空循环而已,发生了什么?!
下面我来一行行代码分析产生这个问题的原因:
先总结一下 PHP 中两条关于引用的两个规则:
- 给引用变量赋值,实际上是给引用所指向的变量赋值
- 一个引用变量可以被修改为对另外一个变量的引用
分析开始:
$data = ['foo', 'bar']; // 循环开始,$item 变量不存在,新建一个$item 变量,且是一个引用变量,它不指向任何变量地址 foreach ($data as &$item) { // loop 1: 执行了 $item = &$data[0];$item 指向 $data[0] 的地址 // loop 2: 执行了 $item = &$data[1];$item 指向 $data[1] 的地址 } // 提示:这个循环没有改变 $data 的数据,只是 $item 依然指向第二个元素 的地址 // 循环开始,$item 变量存在,不会新建变量 foreach ($data as $item) { // loop 1: 执行了 $item = $data[0];$item 所指向的变量(即 第二个元素)的值被修改为$data[0](即'foo'),这里已经导致了$data 两个元素都等于 'foo' // loop 2: 执行了 $item = $data[1];由于$item 指向的是$data[1],实际上相当于执行$data[1] = $data[1],没有任何意义 } // 最后$data 中的两个元素都是 'foo' 如何避免这个问题:
foreach ($data as &$item) { // 每次 loop 销毁$item (实际上只要在最后一次 loop 销毁即可,因此你可以把 unset 写到 foreach 后面,就是不是很好看) unset($item); } ~这里没有二维码和其他链接~
如何避免这个问题2:
不使用引用。
1 lovecy 2020-12-02 18:23:33 +08:00 这个问题太经典了,一些老代码这样写,被坑了好几次 我觉得最好就不要用引用。。。用 array_keys 遍历都好一点 |
2 kidlj 2020-12-02 18:26:55 +08:00 加入收藏来警示自己:永远不要学 PHP 。 |
3 MengiNo 2020-12-02 18:31:06 +08:00 $data as $value 的 value 变量名别用一样的就没事,哪怕三个 foreach 分别写成 $value 、$val 、$v 即可,个人更习惯根据不同的逻辑起更具体的名字。多写 unset 在绝大多数场景不需要而且比较丑,但是老是要记着可能会出现这种问题心智负担又很重。 |
4 oneonesv 2020-12-02 18:34:04 +08:00 如何避免这个问题: 不用引用 |
5 sagaxu 2020-12-02 18:37:58 +08:00 via Android item 的作用域不是应该只在 foreach 内吗 |
6 junan0708 2020-12-02 19:03:39 +08:00 via Android 某公司的笔试题 |
7 AngryPanda 2020-12-02 19:05:19 +08:00 几百年前的题目了 |
8 sleepm 2020-12-02 19:18:41 +08:00 最小化分析代码粘到 artisan tinker 里输出的是 foo 和 bar Psy Shell v0.9.3 (PHP 7.2.24-0ubuntu0.18.04.7 cli) |
9 xiangyuecn 2020-12-02 19:18:58 +08:00 拥有显式的 unset 函数,却没有地方强制要求声明变量,php 可怕就可怕在这个地方 你说这玩意是简化代码编写嘛,一堆$看着碍眼,想想就要笑 题不题的无所谓(居然还被做成了题),本质上是语言的缺陷,好了你掌握了避开了,就镀一层金叫:技能 php 多一个 var 或 let 也行啊 新声明就自动 unset 老的,或直接报错,多好。不管你有多少年经验,这种问题避免不了的,只要代码是人写的!!! |
10 sleepm 2020-12-02 19:20:41 +08:00 php test.php 是 foo foo 学习了 |
11 sleepm 2020-12-02 19:26:08 +08:00 二楼三楼说的对, foreach ($arr as $k => $v ){ $arr[$k] = $v + 1; } 这样在循环内修改原数组比较安全 其实在循环外 unset 也是可以的,不过修改变量名不是更简单么 |
12 ben1024 2020-12-02 19:55:03 +08:00 1.不建议使用引用 2.如果为了性能使用及时释放引用内存变量,或者在闭包中使用 |
13 JJstyle OP @sagaxu php5.6 是会结束循环后依然保留$item 的,7.x 不清楚,我回去再尝试一下 |
14 dobelee 2020-12-02 20:14:49 +08:00 via iPhone @kidlj #2 这个问题所有语言都有。php 算是比较不容易出现的了,因为要显式加取地址符,容易排查,而 go 之类的大部分情况数组本身传递的就是指针。新手基本都要踩坑。 |
17 JJstyle OP @sleepm 你确定吗,我在 thinker 下执行还是 foo foo ( Psy Shell v0.9.12 (PHP 7.2.32 cli)) |
18 JJstyle OP @sagaxu 是的,尝试执行如下 js 代码会报错: ```js for (let n of [1,2]) { } console.log(n); ``` ReferenceError: n is not defined |
19 lovecy 2020-12-03 15:48:42 +08:00 @xiangyuecn php 很多语法是搬的 shell 的,$这个你该问问几十年前的前辈,现在新的语言都有 let 、var,但或许不该嘲笑以前流传下来的东西 |
20 sleepm 2020-12-03 23:25:50 +08:00 |
21 sleepm 2020-12-03 23:27:28 +08:00 |
22 ChoateYao 2020-12-07 19:42:59 +08:00 因为 foreach 申明的是一个在该上下文中的全局变量,一般的建议是不要使用相同的变量名。 包括 for 语法也一样。 主要还是 PHP 中 foreach 、for 这类语法的代码块中声明的变量不属于 foreach 、for 的上下文,所以你能在 foreach 、for 之外使用 foreach 、for 中声明的变量。 |
23 Varobjs 2020-12-08 11:12:02 +08:00 不是引用到锅, 是习惯,不管后面有没有再用$item 都要 unset,这是好习惯 |