值传递、引用传递与可变参数 ... 的使用场景
解读
这道题在国内 PHP 面试中出现频率极高,面试官通常用 2~3 分钟快速追问“什么时候用 &、什么时候用 ...、性能如何、坑点在哪”。
核心目的:
- 验证你对 PHP 变量底层(zval、写时复制、refcount)是否了解;
- 判断你是否能在高并发、大数据量场景下正确选择参数传递方式,避免“内存爆炸”或“数据污染”;
- 观察你是否熟悉现代框架(Laravel、Hyperf)源码里可变参数的典型用法,能否举一反三。
回答思路:先给定义→再给场景→再给代码片段→最后带一句“踩坑提醒”,节奏控制在 90 秒内,让面试官可以立即追问细节。
知识点
- 值传递(默认):函数形参是原变量的“拷贝”,修改不影响外部;触发写时复制(COW),内存延迟增加。
- 引用传递(&):形参绑定原变量别名,函数内修改即外部修改; refcount 直接+1,不产生拷贝,但破坏封装,单元测试难写。
- 可变参数 ... :
3.1 形参列表...$args把实参打包成数组,保持值传递语义;
3.2 调用时...$array把数组解包成实参,可与位置参数混用;
3.3 在 7.0+ 支持类型约束function foo(int ...$ids),JIT 下性能接近原生数组。 - 场景口诀:
“读多写少”用值传递;
“大数组且需回写”用引用;
“不定参数且类型安全”用 ...;
“框架级扩展”用 ...+引用组合,例如中间件栈、事件监听器。 - 坑点:
& 传递后 unset 只断别名,不会释放外部变量;
... 打包后的数组是连续数字索引,关联键丢失;
在 foreach 中同时使用 & 和 ... 容易出现“引用残留”,导致下一次循环数据异常。
答案
- 值传递
场景:对外部数据只读,或函数内部需要无副作用的局部副本。
示例:订单金额格式化,保证原订单对象不被篡改。
function formatPrice(float $amount): string {
$amount = round($amount, 2); // 修改的是拷贝
return '¥' . $amount;
}
踩坑提醒:如果传入的是 100K 的数组,PHP 并不会立即复制,但后续写操作会触发 COW,峰值内存可能翻倍,批量处理大文件时需评估。
- 引用传递
场景:需要在一个函数内对超大数组或对象进行“原地”修改,并让调用方立即可见;或者返回多个结果(PHP 不支持多返回值时常用)。
示例:对 10 万条日志记录实时累加权重,避免复制数组。
function accumulate(array &$logs, float $weight): void {
foreach ($logs as &$log) {
$log['score'] += $weight;
}
}
踩坑提醒:
- 引用破坏纯函数,并行请求下可能产生竞态,必须加锁或复制;
- 单元测试需要手动还原数据,否则用例间互相污染;
- 与 Opcache 兼容,但 HHVM 下引用路径有 BUG,国内部分老旧 CDN 节点仍跑 HHVM,需要回归验证。
- 可变参数 ...
场景:
a) 包装第三方回调,如中间件、事件系统,参数个数由开发者决定;
b) 实现轻量级 DAO,支持 whereIn 可变 ID 列表;
c) 兼容旧版 __call 魔术方法,减少 func_get_args() 调用开销。
示例:Hyperf 事件派发器简化版。
class EventDispatcher {
public function dispatch(string $event, ...$args): void {
foreach ($this->listeners[$event] ?? [] as $listener) {
$listener(...$args); // 解包调用,保持类型
}
}
}
踩坑提醒:
- ... 打包后一定是数组,空实参得到 [] 而非 null,需用空数组判断;
- 与引用不能混用,PHP8 起
function foo(&...$args)直接语法错误; - 在 7.0 之前版本用 func_get_args() 性能差 30%,老项目升级时要压测。
拓展思考
-
性能对比:
在 PHP8.2 + JIT 下,对 100 万元素数组分别做“值传递+写一次”、“引用传递”、“...打包只读”三轮压测,引用传递耗时最低(0.18 s),但内存峰值与值传递持平(写时复制未触发);...打包因额外数组对象,耗时 0.22 s,却带来类型安全,适合框架层。
结论:业务层优先值传递,大数据回写再选引用,框架层用 ... 换可维护性。 -
与协程冲突:
在 Swoole 4 协程环境下,引用传递会把同一数组别名共享给多个协程,导致数据错乱;官方建议:
- 数组用值传递+返回值;
- 对象默认句柄传递,无需 &;
- 必须回写时用 Channel 或 ConnectionContext 隔离,而非 &。
- 面试加分项:
主动提到“我阅读过 Laravel Pipeline 源码,其then()方法用...$passable解包,避免中间件对请求对象写时复制,从而把 2 MB 请求体内存占用降到 0.3 MB”,能让面试官立即追问源码行号,顺利把话题引入框架设计,拉开与其他候选人的差距。