最小化分析代码:
$data = ['foo', 'bar'];
foreach ($data as &$item) {
}
foreach ($data as $item) {
}
print_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,这是好习惯 |