Skip to content

引用传递

概述

引用传递(Pass by Reference)是 PHP 引用机制中最常用的应用场景。通过在函数参数前添加 & 符号,函数可以直接操作调用者的变量,而无需复制数据。本章将系统讲解函数参数引用传递的语法、对象默认传递行为、大数组传递优化等内容。

关键知识

PHP 中的对象默认就是按引用传递的(实际上是传递对象标识符),不需要额外使用 &。引用传递主要用于标量类型、数组和需要就地修改的场景。

基础概念

什么是引用传递

引用传递允许函数直接访问和修改调用方提供的原始变量。与值传递(创建副本)不同,引用传递在函数内部对参数的任何修改都会反映到外部变量。

为什么要使用引用传递

  • 避免大数据复制:处理大数组或大字符串时,引用传递可以避免不必要的内存复制
  • 就地修改:当函数的核心目的就是修改传入的变量时(如排序、过滤)
  • 多返回值:通过多个引用参数模拟多个返回值

何时使用引用传递

使用准则

仅在以下场景使用引用传递:函数的职责就是修改传入的变量(如排序、规范化),或者需要通过参数返回多个值。其他情况下优先使用返回值。

语法与代码

函数参数引用声明

在函数定义中,参数名前加 & 声明引用参数。

php
<?php
declare(strict_types=1);

// 引用传递声明:参数前加 &
function square(int &$number): void
{
    $number *= $number;
}

$value = 5;
square($value);
echo $value; // 输出: 25

多个引用参数

php
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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 — 原数组被修改

最佳实践

  1. 函数签名中明确标注引用:使用 & 前缀让调用者知道参数会被修改。
  2. 对象不需要引用传递:对象默认传递标识符,函数内修改对象属性会直接影响原始对象。
  3. 大数组修改考虑引用:当数组很大且需要在函数内修改时,引用传递比值传递 + 返回更高效。
  4. 避免引用 + 返回值混合:如果参数是引用传递,通常返回 void。混合使用会让调用者困惑。
  5. 在 PHPDoc 中标注:使用 @param-out 或注释说明引用参数的修改效果。
  6. 谨慎使用函数中的静态引用:函数内的静态变量与引用结合使用时要特别注意生命周期。
php
<?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);
}

参考链接