引用做什么
概述
引用在 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]最佳实践
- 优先使用返回值:大多数情况下,通过返回值传递结果比引用传递更清晰。只在确实需要"就地修改"时使用引用。
- 引用参数放在前面:如果一个函数同时有引用参数和普通参数,将引用参数放在前面更符合直觉。
- 文档中标注引用:在 PHPDoc 中使用
@param注明引用参数,提醒调用者该参数会被修改。 - 避免引用链过长:过多的引用嵌套会让代码逻辑难以追踪。
- 测试引用行为:编写单元测试验证引用参数的修改是否符合预期。
- 考虑不可变模式:如果函数的核心目的是变换数据,考虑使用纯函数(不修改输入,返回新值)来代替引用修改。
php
<?php
declare(strict_types=1);
// 推荐:纯函数模式
function normalizeEmail(string $email): string
{
return strtolower(trim($email));
}
// 仅在需要就地修改时才使用引用
function normalizeEmailInPlace(string &$email): void
{
$email = strtolower(trim($email));
}