V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
JJstyle
V2EX  ›  PHP

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

  •  
  •   JJstyle · 2020-12-02 18:09:41 +08:00 · 1928 次点击
    这是一个创建于 673 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最小化分析代码:

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

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

    第 1 条附言  ·  2020-12-03 10:23:47 +08:00
    经过本人测试目前 7.x 都有这个问题:

    7.4 测试结果: http://sandbox.onlinephpfunctions.com/code/9a5d874c1e9ceb5a3cb3f3da2cd8fd8066497644
    第 2 条附言  ·  2020-12-03 10:24:34 +08:00

    如何避免这个问题2:

    不使用引用。

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

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

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

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

    包括 for 语法也一样。

    主要还是 PHP 中 foreach 、for 这类语法的代码块中声明的变量不属于 foreach 、for 的上下文,所以你能在 foreach 、for 之外使用 foreach 、for 中声明的变量。
    Varobjs
        23
    Varobjs  
       2020-12-08 11:12:02 +08:00
    不是引用到锅,
    是习惯,不管后面有没有再用$item 都要 unset,这是好习惯
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2355 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 45ms · UTC 14:37 · PVG 22:37 · LAX 07:37 · JFK 10:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.