简单分析一下 PHP 中`foreach ($data as &$item) `循环引用产生的问题

2020-12-02 18:09:41 +08:00
 JJstyle

最小化分析代码:

$data = ['foo', 'bar'];

foreach ($data as &$item) {
}

foreach ($data as $item) {
}

print_r($data);

输出结果:

Array
(
    [0] => 'foo'
    [1] => 'foo'
)

我们可以发现,$data的值 ~~莫名奇妙~~ 变了,而它只是经过了两个空循环而已,发生了什么?!

下面我来一行行代码分析产生这个问题的原因:

先总结一下 PHP 中两条关于引用的两个规则:

  1. 给引用变量赋值,实际上是给引用所指向的变量赋值
  2. 一个引用变量可以被修改为对另外一个变量的引用

分析开始:

$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);
}

~这里没有二维码和其他链接~

2408 次点击
所在节点    PHP
23 条回复
lovecy
2020-12-02 18:23:33 +08:00
这个问题太经典了,一些老代码这样写,被坑了好几次
我觉得最好就不要用引用。。。用 array_keys 遍历都好一点
kidlj
2020-12-02 18:26:55 +08:00
加入收藏来警示自己:永远不要学 PHP 。
MengiNo
2020-12-02 18:31:06 +08:00
$data as $value 的 value 变量名别用一样的就没事,哪怕三个 foreach 分别写成 $value 、$val 、$v 即可,个人更习惯根据不同的逻辑起更具体的名字。多写 unset 在绝大多数场景不需要而且比较丑,但是老是要记着可能会出现这种问题心智负担又很重。
oneonesv
2020-12-02 18:34:04 +08:00
如何避免这个问题:
不用引用
sagaxu
2020-12-02 18:37:58 +08:00
item 的作用域不是应该只在 foreach 内吗
junan0708
2020-12-02 19:03:39 +08:00
某公司的笔试题
AngryPanda
2020-12-02 19:05:19 +08:00
几百年前的题目了
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)
xiangyuecn
2020-12-02 19:18:58 +08:00
拥有显式的 unset 函数,却没有地方强制要求声明变量,php 可怕就可怕在这个地方

你说这玩意是简化代码编写嘛,一堆$看着碍眼,想想就要笑😂

题不题的无所谓(居然还被做成了题),本质上是语言的缺陷,好了你掌握了避开了,就镀一层金叫:技能

php 多一个 var 或 let 也行啊 新声明就自动 unset 老的,或直接报错,多好。不管你有多少年经验,这种问题避免不了的,只要代码是人写的!!!
sleepm
2020-12-02 19:20:41 +08:00
php test.php 是 foo foo
学习了
sleepm
2020-12-02 19:26:08 +08:00
二楼三楼说的对,
foreach ($arr as $k => $v ){
$arr[$k] = $v + 1;
}
这样在循环内修改原数组比较安全
其实在循环外 unset 也是可以的,不过修改变量名不是更简单么
ben1024
2020-12-02 19:55:03 +08:00
1.不建议使用引用
2.如果为了性能使用及时释放引用内存变量,或者在闭包中使用
JJstyle
2020-12-02 19:56:21 +08:00
@sagaxu php5.6 是会结束循环后依然保留$item 的,7.x 不清楚,我回去再尝试一下
dobelee
2020-12-02 20:14:49 +08:00
@kidlj #2 这个问题所有语言都有。php 算是比较不容易出现的了,因为要显式加取地址符,容易排查,而 go 之类的大部分情况数组本身传递的就是指针。新手基本都要踩坑。
kidlj
2020-12-02 20:19:53 +08:00
@dobelee 谢谢解答。不过这里更让我难以接受的是循环体内的变量不是单独 scope 的吗?
sagaxu
2020-12-02 20:25:23 +08:00
@dobelee 别的语言习惯用 block scoped,不会踩这种坑
JJstyle
2020-12-03 10:28:09 +08:00
@sleepm 你确定吗,我在 thinker 下执行还是 foo foo ( Psy Shell v0.9.12 (PHP 7.2.32 — cli))
JJstyle
2020-12-03 10:39:34 +08:00
@sagaxu 是的,尝试执行如下 js 代码会报错:

```js
for (let n of [1,2]) {
}
console.log(n);
```

ReferenceError: n is not defined
lovecy
2020-12-03 15:48:42 +08:00
@xiangyuecn php 很多语法是搬的 shell 的,$这个你该问问几十年前的前辈,现在新的语言都有 let 、var,但或许不该嘲笑以前流传下来的东西
sleepm
2020-12-03 23:25:50 +08:00

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/731473

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX