三个变量,一个数组,为何修改一处,处处生效?这不是bug,而是PHP引用机制在幕后导演的一场精妙魔术。
来先看这段引发思考的代码:
$userTags = C::t('common_user_tags_item')->fetch_by_uid($uid, $status, $type);$userTags1 = C::t('common_user_tags_item')->fetch_by_uid($uid, $status, 1);$userTags2 = C::t('common_user_tags_item')->fetch_by_uid($uid, $status, 2); //关键操作:创建引用数组 $tagArrays = array(&$userTags, &$userTags1, &$userTags2);// 处理这个引用数组foreach($tagArrays as &$tags) { // 一些处理逻辑... $tags[$key]['name'] = $someValue; // 这行操作很关键}// 神奇现象://$userTags、$userTags1、$userTags2 也被修改了!$retObj['list'] = $userTags; // 这里拿到的是已经被修改后的数据$retObj['list1'] = $userTags1; // 这里拿到的是已经被修改后的数据$retObj['list2'] = $userTags2; // 这里拿到的是已经被修改后的数据
为什么处理 $tagArrays 后,原始的三个变量也跟着变了?
答案就在那一行不起眼的 & 符号里。
代码中 修改$tagArrays里的引用数组后,原始变量$userTags等也“自动”发生了变化。这是因为PHP的 “引用”(Reference) 机制,而不是普通的值拷贝。

理解“引用”的太空实验
想象你手里有三个太空站模型(对应 $userTags, $offer_userTags, $need_userTags 三个数组)。现在,你制作了一个名为 $tagArrays 的遥控器,这个遥控器上不是模型副本,而是三个可以直接远程操控原始太空站的专用遥控按钮。
// 这三个是“太空站”本身$userTags = [...];$userTags1 = [...];$userTags2 = [...];// 创建“专用遥控器”,按钮直接连着三个太空站$tagArrays = array(&$userTags, &$userTags1, &$userTags2); ↑ ↑ ↑ // 注意这里的 & 符号,它创建了“引用”(遥控线路)
当你用 foreach 操作这个“遥控器”时:
foreach($tagArrays as &$tags) { // 此时$tags是其中一个遥控按钮// 通过这个按钮做的任何操作//(// 如$tags[$key]['name'] = ...//)// 都会通过“遥控线路”直接作用于对应的原始太空站上!}
所以,整个过程中始终在操作原始数组,并没有创建和使用一个需要“传回去”的独立副本。这就是为什么最后不需要 $userTags = $tagArrays[0]; 这样的赋值操作,节省内存且提升性能。
核心原理
- 关键符号 &:代码中 array(&$userTags, ...) 的 & 符号,创建了对原始变量的引用。你可以把它理解为一个别名或快捷方式。$tagArrays[0] 和 $userTags 指向的是内存中的同一块数据。
- 循环中的引用:foreach ($tagArrays as &$tags) 再次使用了 &,这使得在循环中,$tags 直接成为了 $tagArrays 中当前那个引用的别名。所以对 $tags[$key] 的修改,就等同于对 $userTags[$key](或另外两个)的直接修改。
- 为何返回的变量会变:因为 $retObj['list'] = $userTags; 这一行执行时,$userTags 这个“太空站”已经被之前的循环过程通过“遥控器”修改过了。赋值的是已经被修改后的结果。
对比实验:有引用 vs 无引用
// 实验组:使用引用(幽灵分身模式)$data1 = ['id' => 1, 'name' => '测试'];$data2 = ['id' => 2, 'name' => '测试2'];$refArray = [&$data1, &$data2];foreach($refArray as &$item) { $item['processed'] = true; // 直接修改原始数据}echo json_encode($data1); // 输出://{"id":1,"name":"测试","processed":true} 原始数据被修改!// 对照组:不使用引用(复印模式)$data1 = ['id' => 1, 'name' => '测试'];$data2 = ['id' => 2, 'name' => '测试2'];$copyArray = [$data1, $data2]; // 注意:没有 & 符号foreach($copyArray as $item) { $item['processed'] = true; // 只修改副本}echo json_encode($data1); // 输出:{"id":1,"name":"测试"} 原始数据保持不变!
需要注意,这里存在一个潜在隐患
虽然引用强大,但使用不当会引入隐蔽的bug。最常见的问题是循环后的引用残留:
$items = [['val' => 1], ['val' => 2], ['val' => 3]];$refs = [&$items[0], &$items[1], &$items[2]];foreach($refs as &$ref) { // 处理逻辑}// 此时 $ref 仍然引用着 $items[2]!$ref = ['val' => 999]; // 危险!这会意外修改 $items[2]// 正确做法:及时销毁引用foreach($refs as &$ref) { // 处理逻辑}unset($ref); // 安全:断开引用链接
最佳实践建议:
- 明确意图:使用引用时,要明确自己确实需要“幽灵分身”特性
- 及时清理:循环使用引用后,立即 unset() 循环变量
- 慎用返回引用:除非必要,函数不要返回引用
- 文档说明:使用引用的地方添加注释,说明设计意图
引用的正确应用场景
引用并非洪水猛兽,在适当场景下能显著提升性能:
场景一:避免大数组复制
$items = [['val' => 1], ['val' => 2], ['val' => 3]];$refs = [&$items[0], &$items[1], &$items[2]];foreach($refs as &$ref) { // 处理逻辑}// 此时 $ref 仍然引用着 $items[2]!$ref = ['val' => 999]; // 危险!这会意外修改 $items[2]// 正确做法:及时销毁引用foreach($refs as &$ref) { // 处理逻辑}unset($ref); // 安全:断开引用链接
场景二:在循环中修改原始数组
$items = [['val' => 1], ['val' => 2], ['val' => 3]];$refs = [&$items[0], &$items[1], &$items[2]];foreach($refs as &$ref) { // 处理逻辑}// 此时 $ref 仍然引用着 $items[2]!$ref = ['val' => 999]; // 危险!这会意外修改 $items[2]// 正确做法:及时销毁引用foreach($refs as &$ref) { // 处理逻辑}unset($ref); // 安全:断开引用链接
场景三:实现链式调用
$items = [['val' => 1], ['val' => 2], ['val' => 3]];$refs = [&$items[0], &$items[1], &$items[2]];foreach($refs as &$ref) { // 处理逻辑}// 此时 $ref 仍然引用着 $items[2]!$ref = ['val' => 999]; // 危险!这会意外修改 $items[2]// 正确做法:及时销毁引用foreach($refs as &$ref) { // 处理逻辑}unset($ref); // 安全:断开引用链接
PHP的引用机制是一把双刃剑。它既不是大多数初学者应该首先接触的概念,也不是应该完全回避的“禁区”。
理解引用的关键在于建立正确的心智模型:引用不是数据副本,而是数据的别名。当你在代码中看到 & 符号时,应该立即想到“幽灵分身”——这两个变量共享着同一份生命。
需要在PHP中高效处理多个数组时,不妨考虑是否可以使用引用机制来避免不必要的内存复制。但同时,也要时刻警惕引用可能带来的副作用,养成及时 unset() 引用变量的好习惯。