Skip to content

引用做什么

概述

引用在 PHP 中主要有三种用途:引用赋值、引用传递和引用返回。理解引用"做什么"——即引用在不同场景下的具体行为和效果——是正确使用引用的关键。本章将深入探讨引用赋值和引用传递的内部工作机制,帮助你理解两个变量如何通过引用指向同一份数据,以及这种行为在实际编程中的影响。

核心理解

引用的本质是"让两个变量名共享同一份数据"。修改其中任何一个,所有引用该数据的变量都会同步更新。

基础概念

引用赋值的含义

引用赋值意味着两个变量名不再独立,而是共享同一个值。此后对任何一个变量的修改都会反映到另一个变量上。

引用传递的含义

引用传递是将函数外部变量的引用传递给函数内部,使得函数可以直接修改外部变量。在函数定义中,参数前加上 & 即可声明引用参数。

共享数据的生命周期

被引用的数据的生命周期由引用计数管理。只要还有一个变量名引用该数据,数据就不会被释放。当所有引用都断开后,数据才会被垃圾回收。

语法与代码

引用赋值:两变量指向同一数据

php
<?php
declare(strict_types=1);

// 引用赋值的基本语法
$name = 'Alice';
$alias = &$name; // $alias 成为 $name 的别名

echo $name;  // 输出: Alice
echo $alias; // 输出: Alice

// 通过 $alias 修改,$name 也随之改变
$alias = 'Bob';
echo $name;  // 输出: Bob
echo $alias; // 输出: Bob

引用传递:函数内部修改外部变量

php
<?php
declare(strict_types=1);

// 引用传递函数
function addTax(float &$price, float $taxRate): void
{
    $price += $price * $taxRate;
}

$amount = 100.0;
addTax($amount, 0.1);

echo $amount; // 输出: 110 — 原始变量被修改了

引用传递 vs 值传递的对比

php
<?php
declare(strict_types=1);

// 值传递:函数内部修改不影响外部
function doubleValue(int $value): int
{
    $value *= 2;
    return $value;
}

// 引用传递:函数内部修改直接影响外部
function doubleByReference(int &$value): void
{
    $value *= 2;
}

$num = 5;

// 值传递需要接收返回值
$result = doubleValue($num);
echo "num: {$num}, result: {$result}\n"; // num: 5, result: 10

// 引用传递直接修改原变量
doubleByReference($num);
echo "num: {$num}\n"; // num: 10

多变量引用同一数据

php
<?php
declare(strict_types=1);

// 三个变量引用同一份数据
$config = ['timeout' => 30, 'retries' => 3];
$cfgRef1 = &$config;
$cfgRef2 = &$config;

// 通过任意引用修改,所有引用都可见变化
$cfgRef2['timeout'] = 60;

print_r($config);
// Array ( [timeout] => 60 [retries] => 3 )

print_r($cfgRef1);
// Array ( [timeout] => 60 [retries] => 3 )

引用赋值与重新赋值的区别

php
<?php
declare(strict_types=1);

$a = 'hello';
$b = &$a;

// 引用断开后重新赋值
$b = 'world';  // 修改共享数据,$a 也变为 'world'
unset($b);     // 断开 $b 的引用,$a 不受影响
$b = 'new';    // $b 现在是独立的新变量

echo $a; // 输出: world
echo $b; // 输出: new

详细说明

引用传递的实际过程

当调用一个带有引用参数的函数时,PHP 将外部变量的引用绑定到函数内部的参数变量上。函数执行期间,内部参数和外部变量指向同一份数据。函数执行完毕后,引用绑定解除。

调用前:  $external → [value: 10]
调用函数 func(&$external)
函数内:  $param → $external → [value: 10]
修改:    $param = 20
         $param → $external → [value: 20]
函数返回:$external → [value: 20]

引用传递与值传递的内存行为对比

操作值传递引用传递
赋值给函数参数Copy-on-Write(共享,修改时复制)绑定引用(无复制)
函数内修改参数不影响外部变量直接修改外部变量
内存开销修改时产生副本无额外开销
返回值需要返回新值void 即可

引用赋值在数组上的行为

引用赋值对数组有一些特殊行为需要注意。数组的值拷贝是浅拷贝(shallow copy),但如果数组元素本身就是引用,则引用关系会被保留。

php
<?php
declare(strict_types=1);

// 数组赋值的引用行为
$original = [1, 2, 3];
$ref = &$original;

// 修改数组本身
$ref[] = 4;
print_r($original);
// Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 4 )

// 重新赋值整个数组
$ref = ['a', 'b'];
echo $original[0]; // 输出: a

函数默认参数与引用

引用参数可以有默认值,但默认值只在没有传入参数时使用。一旦传入了参数(即使是引用),默认值不生效。

php
<?php
declare(strict_types=1);

function appendSuffix(string &$text, string $suffix = '...'): void
{
    $text .= $suffix;
}

$message = 'Hello';
appendSuffix($message);         // 使用默认后缀
echo $message; // 输出: Hello...

appendSuffix($message, '!');    // 使用指定后缀
echo $message; // 输出: Hello...!

实战示例

实用场景:模板变量替换

php
<?php
declare(strict_types=1);

function replaceTemplate(
    string &$template,
    array $variables
): void {
    foreach ($variables as $key => $value) {
        $template = str_replace(
            "{{{$key}}}",
            (string) $value,
            $template
        );
    }
}

$html = '<p>Hello, {{name}}! Your balance is {{balance}}.</p>';
replaceTemplate($html, [
    'name' => 'Alice',
    'balance' => '$1,000',
]);

echo $html;
// <p>Hello, Alice! Your balance is $1,000.</p>

实用场景:数组排序并保持键名

php
<?php
declare(strict_types=1);

function sortByName(array &$users): void
{
    uasort($users, function (array $a, array $b): int {
        return $a['name'] <=> $b['name'];
    });
}

$team = [
    ['id' => 3, 'name' => 'Charlie'],
    ['id' => 1, 'name' => 'Alice'],
    ['id' => 2, 'name' => 'Bob'],
];

sortByName($team);

print_r($team);
// Array (
//     [1] => Array ( [id] => 1 [name] => Alice )
//     [2] => Array ( [id] => 2 [name] => Bob )
//     [0] => Array ( [id] => 3 [name] => Charlie )
// )

实用场景:多值回调模式

php
<?php
declare(strict_types=1);

function processData(
    array $input,
    callable $transform,
    array &$successes,
    array &$failures
): void {
    foreach ($input as $item) {
        try {
            $successes[] = $transform($item);
        } catch (\Throwable $e) {
            $failures[] = ['item' => $item, 'error' => $e->getMessage()];
        }
    }
}

$records = [1, 2, 3, 'invalid', 4, 5];
$valid = [];
$invalid = [];

processData(
    $records,
    fn(int $v): int => $v * $v,
    $valid,
    $invalid
);

print_r($valid);   // Array ( [0] => 1 [1] => 4 [2] => 9 [3] => 16 [4] => 25 )
print_r($invalid); // Array ( [0] => Array ( [item] => invalid [error] => ... ) )

实用场景:构建器模式中的引用收集

php
<?php
declare(strict_types=1);

class QueryBuilder
{
    private array $conditions = [];
    private array $bindings = [];

    public function where(string $condition, array &$bindings): self
    {
        $this->conditions[] = $condition;
        $bindings = array_merge($bindings, $this->extractBindings($condition));
        return $this;
    }

    private function extractBindings(string $condition): array
    {
        preg_match_all('/:(\w+)/', $condition, $matches);
        return array_map(fn(string $m) => ":{$m}", $matches[1] ?? []);
    }

    public function getConditions(): array
    {
        return $this->conditions;
    }
}

$params = [];
$query = (new QueryBuilder())
    ->where('age > :age', $params)
    ->where('status = :status', $params);

print_r($params);
// Array ( [0] => :age [1] => :status )

注意事项

引用参数不能有类型为字面量的默认值

php
<?php
declare(strict_types=1);

// 正确:引用参数使用 null 或标量默认值
function setDefault(int &$value, ?int $default = null): void
{
    if ($value === 0) {
        $value = $default ?? 42;
    }
}

// 注意:在严格模式下,传入的类型必须匹配声明类型
$val = 0;
setDefault($val, 100);
echo $val; // 输出: 100

引用传递与 call_user_func_array

当使用 call_user_func_array 时,引用传递需要注意参数传递方式。PHP 8.0+ 对此行为有所改进。

兼容性

在 PHP 8.0 之前,call_user_func_array 对引用参数的行为不一致。PHP 8.0+ 中,严格模式下引用参数的行为更加可预测。

嵌套函数调用中的引用传递

php
<?php
declare(strict_types=1);

function innerModify(string &$data): void
{
    $data .= ' [inner]';
}

function outerProcess(string &$data): void
{
    $data .= ' [outer]';
    innerModify($data); // 引用继续传递
}

$text = 'base';
outerProcess($text);
echo $text; // 输出: base [outer] [inner]

最佳实践

  1. 优先使用返回值:大多数情况下,通过返回值传递结果比引用传递更清晰。只在确实需要"就地修改"时使用引用。
  2. 引用参数放在前面:如果一个函数同时有引用参数和普通参数,将引用参数放在前面更符合直觉。
  3. 文档中标注引用:在 PHPDoc 中使用 @param 注明引用参数,提醒调用者该参数会被修改。
  4. 避免引用链过长:过多的引用嵌套会让代码逻辑难以追踪。
  5. 测试引用行为:编写单元测试验证引用参数的修改是否符合预期。
  6. 考虑不可变模式:如果函数的核心目的是变换数据,考虑使用纯函数(不修改输入,返回新值)来代替引用修改。
php
<?php
declare(strict_types=1);

// 推荐:纯函数模式
function normalizeEmail(string $email): string
{
    return strtolower(trim($email));
}

// 仅在需要就地修改时才使用引用
function normalizeEmailInPlace(string &$email): void
{
    $email = strtolower(trim($email));
}

参考链接