引用传递
概述
引用传递(Pass by Reference)是 PHP 引用机制中最常用的应用场景。通过在函数参数前添加 & 符号,函数可以直接操作调用者的变量,而无需复制数据。本章将系统讲解函数参数引用传递的语法、对象默认传递行为、大数组传递优化等内容。
关键知识
PHP 中的对象默认就是按引用传递的(实际上是传递对象标识符),不需要额外使用 &。引用传递主要用于标量类型、数组和需要就地修改的场景。
基础概念
什么是引用传递
引用传递允许函数直接访问和修改调用方提供的原始变量。与值传递(创建副本)不同,引用传递在函数内部对参数的任何修改都会反映到外部变量。
为什么要使用引用传递
- 避免大数据复制:处理大数组或大字符串时,引用传递可以避免不必要的内存复制
- 就地修改:当函数的核心目的就是修改传入的变量时(如排序、过滤)
- 多返回值:通过多个引用参数模拟多个返回值
何时使用引用传递
使用准则
仅在以下场景使用引用传递:函数的职责就是修改传入的变量(如排序、规范化),或者需要通过参数返回多个值。其他情况下优先使用返回值。
语法与代码
函数参数引用声明
在函数定义中,参数名前加 & 声明引用参数。
<?php
declare(strict_types=1);
// 引用传递声明:参数前加 &
function square(int &$number): void
{
$number *= $number;
}
$value = 5;
square($value);
echo $value; // 输出: 25多个引用参数
<?php
declare(strict_types=1);
// 多个引用参数
function divideIntoParts(
float $total,
int $parts,
float &$quotient,
float &$remainder
): void {
$quotient = intdiv((int) $total, $parts);
$remainder = $total - ($quotient * $parts);
}
$total = 10.0;
$quot = 0.0;
$rem = 0.0;
divideIntoParts($total, 3, $quot, $rem);
echo "商: {$quot}, 余: {$rem}\n";
// 商: 3, 余: 1对象默认引用传递
<?php
declare(strict_types=1);
class ShoppingCart
{
/** @var array<string, int> */
private array $items = [];
public function add(string $product, int $quantity): void
{
$this->items[$product] = ($this->items[$product] ?? 0) + $quantity;
}
public function getItems(): array
{
return $this->items;
}
}
function addItemToCart(ShoppingCart $cart): void
{
// 无需 &$cart — 对象默认传递标识符
$cart->add('Widget', 1);
}
$myCart = new ShoppingCart();
$myCart->add('Gadget', 2);
addItemToCart($myCart);
print_r($myCart->getItems());
// Array ( [Gadget] => 2 [Widget] => 1 )对象传递机制
PHP 中的对象变量存储的是对象标识符(Object Handle),而非对象本身。将对象变量传递给函数时,传递的是这个标识符的副本,但标识符指向的是同一个对象。因此函数内通过 $cart->add(...) 修改的是原始对象。这与 C++ 中的引用传递不同,PHP 的对象传递更像是"传递指针的副本"。
大数组引用传递优化
<?php
declare(strict_types=1);
// 处理大数组时,引用传递避免复制
function filterLargeArray(array &$data, callable $filter): void
{
foreach ($data as $key => $value) {
if (!$filter($value)) {
unset($data[$key]);
}
}
}
// 生成大型数组
$bigData = range(1, 1000000);
// 使用引用传递,避免复制整个数组
filterLargeArray($bigData, fn(int $v): bool => $v % 2 === 0);
echo count($bigData); // 输出: 500000引用传递与可变参数
<?php
declare(strict_types=1);
// 引用传递与可变参数(PHP 5.6+)
function mergeAll(array &$target, array ...$sources): void
{
foreach ($sources as $source) {
foreach ($source as $key => $value) {
$target[$key] = $value;
}
}
}
$base = ['a' => 1];
mergeAll($base, ['b' => 2], ['c' => 3], ['d' => 4]);
print_r($base);
// Array ( [a] => 1 [b] => 2 [c] => 3 [d] => 4 )详细说明
引用传递的类型限制
PHP 的引用参数可以有类型声明。在严格模式下,传入的变量必须满足声明的类型约束。
<?php
declare(strict_types=1);
// 引用参数可以带类型声明
function incrementCounter(int &$counter, int $step = 1): void
{
$counter += $step;
}
$count = 10;
incrementCounter($count, 5);
echo $count; // 输出: 15
// 严格模式下的类型安全
// incrementCounter('string'); // TypeError引用传递与严格模式
在 declare(strict_types=1) 下,引用传递的类型检查发生在函数调用时。如果传入的变量类型与声明不匹配,将抛出 TypeError。
引用传递与命名参数(PHP 8.0+)
<?php
declare(strict_types=1);
function configure(
string &$host,
int &$port,
string $protocol = 'tcp'
): void {
$host = strtolower($host);
if ($port < 1 || $port > 65535) {
$port = 80;
}
}
$server = 'LOCALHOST';
$portNum = 90000;
// 使用命名参数
configure(host: $server, port: $portNum);
echo "{$server}:{$portNum}\n"; // localhost:80值传递 vs 引用传递的性能对比
<?php
declare(strict_types=1);
// 值传递:大数组赋值触发 Copy-on-Write
function processByValue(array $data): array
{
// 如果不修改 $data,不会复制
// 一旦修改,触发复制
$data[] = 'processed';
return $data;
}
// 引用传递:直接操作原数组
function processByReference(array &$data): void
{
$data[] = 'processed';
}
// 性能考量:对于只读操作,值传递与引用传递性能相近
// 对于修改操作,引用传递避免了复制开销性能建议
对于只读场景(函数不修改参数),值传递由于 Copy-on-Write 机制不会有额外开销。对于需要修改的场景,引用传递避免了复制,但对调用方来说语义不够明确。在实际开发中,除非数据量非常大且需要修改,否则优先使用值传递 + 返回值。
实战示例
实战:CSV 数据解析器
<?php
declare(strict_types=1);
function parseCsvLine(
string $line,
string $delimiter = ',',
array &$fields = [],
int &$fieldCount = 0
): bool {
$fields = str_getcsv($line, $delimiter);
$fieldCount = count($fields);
return $fieldCount > 0;
}
$csvLine = 'Alice,30,alice@example.com';
$parsed = [];
$count = 0;
parseCsvLine($csvLine, ',', $parsed, $count);
echo "字段数: {$count}\n";
print_r($parsed);
// 字段数: 3
// Array ( [0] => Alice [1] => 30 [2] => alice@example.com )实战:构建树形结构
<?php
declare(strict_types=1);
function buildTree(array &$items, string $idKey = 'id', string $parentKey = 'parent_id'): array
{
$tree = [];
$references = [];
foreach ($items as &$item) {
$references[$item[$idKey]] = &$item;
}
unset($item); // 断开 foreach 引用
foreach ($items as &$item) {
if (isset($item[$parentKey]) && isset($references[$item[$parentKey]])) {
$references[$item[$parentKey]]['children'][] = &$item;
} else {
$tree[] = &$item;
}
}
unset($item);
return $tree;
}
$flat = [
['id' => 1, 'parent_id' => null, 'name' => 'Root'],
['id' => 2, 'parent_id' => 1, 'name' => 'Child A'],
['id' => 3, 'parent_id' => 1, 'name' => 'Child B'],
['id' => 4, 'parent_id' => 2, 'name' => 'Grandchild'],
];
$tree = buildTree($flat);
// 结果: Root -> [Child A -> [Grandchild], Child B]实战:数组归一化处理
<?php
declare(strict_types=1);
function normalizeArray(
array &$data,
callable $normalizer,
int &$skippedCount = 0,
int &$modifiedCount = 0
): void {
foreach ($data as $key => &$value) {
$normalized = $normalizer($value);
if ($normalized === $value) {
$skippedCount++;
} else {
$value = $normalized;
$modifiedCount++;
}
}
unset($value); // 断开 foreach 引用
}
$records = [' Alice ', 'bob', 'CHARLIE', 'david'];
$skipped = 0;
$modified = 0;
normalizeArray($records, fn(string $v): string => ucfirst(strtolower(trim($v))), $skipped, $modified);
print_r($records);
// Array ( [0] => Alice [1] => Bob [2] => Charlie [3] => David )
echo "修改: {$modified}, 跳过: {$skipped}\n";
// 修改: 4, 跳过: 0注意事项
常量不能作为引用参数
<?php
declare(strict_types=1);
function modify(int &$value): void
{
$value += 10;
}
// modify(42); // Fatal Error: 不能将常量表达式通过引用传递
// modify(3.14); // Fatal Error
$variable = 42;
modify($variable); // 正确:变量可以按引用传递表达式不能作为引用参数(PHP 8.0 前)
PHP 版本差异
PHP 8.0 之前,不能将表达式(如函数返回值、计算结果)按引用传递。PHP 8.0+ 允许在更多场景下使用引用传递,但仍有一些限制。
引用参数在匿名函数中的使用
<?php
declare(strict_types=1);
// 匿名函数可以使用引用(use 引用)
$counter = 0;
$incrementer = function() use (&$counter): int {
return ++$counter;
};
echo $incrementer(); // 1
echo $incrementer(); // 2
echo $incrementer(); // 3
echo $counter; // 3 — 原始变量被修改引用传递与 array_map
array_map 不支持对数组元素进行引用操作。如需修改数组元素,使用 foreach 引用遍历或 array_walk。
<?php
declare(strict_types=1);
$data = [1, 2, 3, 4, 5];
// array_map 返回新数组,不修改原数组
$doubled = array_map(fn(int $v): int => $v * 2, $data);
echo $data[0]; // 1 — 原数组未变
echo $doubled[0]; // 2 — 新数组
// array_walk 可以修改原数组(通过引用)
array_walk($data, function(int &$value): void {
$value *= 2;
});
echo $data[0]; // 2 — 原数组被修改最佳实践
- 函数签名中明确标注引用:使用
&前缀让调用者知道参数会被修改。 - 对象不需要引用传递:对象默认传递标识符,函数内修改对象属性会直接影响原始对象。
- 大数组修改考虑引用:当数组很大且需要在函数内修改时,引用传递比值传递 + 返回更高效。
- 避免引用 + 返回值混合:如果参数是引用传递,通常返回
void。混合使用会让调用者困惑。 - 在 PHPDoc 中标注:使用
@param-out或注释说明引用参数的修改效果。 - 谨慎使用函数中的静态引用:函数内的静态变量与引用结合使用时要特别注意生命周期。
<?php
declare(strict_types=1);
/**
* 对数组进行原地排序。
*
* @param array<int, mixed> $items 待排序数组(按引用修改)
* @param callable(mixed, mixed): int $comparator 比较函数
*/
function customSort(array &$items, callable $comparator): void
{
usort($items, $comparator);
}