一份记录笔、胶带、回形针数量的文具库存,补货逻辑给每类商品加 2,随后再遍历一次输出最新库存。第一轮为了原地修改数组元素用了引用符号 &,第二轮只做打印、没有加引用。两轮循环却共用了同一个变量名,于是原本 3+2=5 的回形针,输出时变成了 7,而且数组里的值也被同步篡改。

以下四段示例各自独立,依次保存为 example.php 后用 php example.php 运行即可,除 PHP 命令行外不依赖其他组件。

现象:第一个循环算对了,第二个循环把数组改坏了

输出结果里 pen 为 10、tape 为 7、clip 显示 7。回形针的期望值是 5,却被写成 7,而且 $stock['clip'] 本身已是 7,问题不在显示层。

补货那一轮并没有出错,循环收尾时三项数量确实是 10、7、5。变化发生在之后——需要检查 $quantity 这个变量在循环结束后还留着什么状态。

foreach ($stock as &$quantity) 每轮迭代都会让 $quantity 与当前数组元素共享同一份值。走到最后一轮,该变量与 $stock['clip'] 就是同一数据的两条访问路径。循环退出不会自动切断这层关联,PHP 官方手册在 foreach 的引用部分对此有专门警示。

原因:第二轮的赋值动作仍在写同一份内存

第二轮即使不写 &,仍要把当次元素的值赋给 $quantity;而该变量已与回形针绑定,赋值即回写。读笔时写成 10,读胶带时写成 7,轮到回形针时读到的便已是 7。容易误判之处在于:第二轮循环体只包含 echo,真正的写入发生在 foreach 给循环变量赋值的那一步(PHP 手册 What References Do 对向已有引用变量赋值有相应说明)。

不用两个 foreach 也能复现。把第二轮换成一次普通赋值,后果更直接;想清零临时变量,实际清掉的是回形针库存。即使写 $quantity = null 也不会解绑,只会让该元素变成 null。

PHP引用遍历后未unset,回形针库存为何多变2个

解决:循环结束后立即 unset 解绑

正确做法是在引用循环结束之后、再次使用该变量之前调用 unset($quantity)。加上这一步后,输出数值与数组内容都正确,后续对 $quantity 重新赋值亦不再牵连最后一项(参照 PHP 手册 Unsetting References)。

要注意 unset 删除的是变量名与那份值之间的绑定,并非删除数组元素:$stock['clip'] 仍存在,值仍为 5。真正删除元素必须写 unset($stock['clip']),两种写法不可混用。

就地改名成 $displayQuantity 也能让本次输出不再覆盖库存,然而旧的 $quantity 依旧处于绑定状态,日后再赋值仍会出错,所以改名替代不了解绑。

还有一个边界情况:若第一轮因 break 提前退出,留存下来的是最后一次实际迭代到的元素,未必是全数组的末项。只要同作用域内的变量还会被继续使用,就要执行清理,不能因为遍历未完成就假定没有残留。

替代写法:按键写回,不再依赖引用

本次补货逻辑只是给当前值加 2,完全不必让变量长期指向数组元素。直接在循环内写入 $stock[$sku] 即可完成更新。该示例不产生引用,循环后再改 $quantity 也不会影响数组。这里的元素是整数,遍历中也没有增删元素,因此这套写法不必推广成所有数组遍历的通用方案。

对于这类简单更新,按键写回的可读性更高:修改位置一目了然,无需记着循环结束后还要清理一份引用。若既存代码已经采用引用式遍历,则应把 unset($quantity) 紧跟在循环后面。

排查此类问题时优先检查两点:①(编号预留)循环结束时的数组是否仍然正确;②后续代码是否把这个循环变量再次赋值。若两轮之间数据正确、展示以后才变,就不应只盯着补货计算找原因。

小结

小结:PHP 的 foreach 引用循环不会自动解绑,循环变量与末元素持续共享内存,后续赋值会静默篡改数组。每个引用遍历之后紧跟 unset,或在能按键写回的场合放弃引用,是避免数据被二次修改的两种确定性做法(依据:PHP 官方手册 foreach、What References Do、Unsetting References 三节说明)。