参数展开
概述
参数展开(Argument Unpacking)是 PHP 5.6 引入的特性,允许在调用函数时使用 ... 运算符将数组或 Traversable 对象展开为独立的参数。这与可变参数(收集参数)是互补操作:可变参数将多个值打包为数组,参数展开将数组拆包为多个值。
PHP 8.1 进一步扩展了参数展开的能力,支持展开包含命名参数的关联数组。
PHP 版本说明
- 基本参数展开自 PHP 5.6 引入
- 展开 Traversable 对象自 PHP 5.6 起支持
- 展开含命名参数的关联数组自 PHP 8.0 引入
- 在
...后使用命名参数覆盖已展开值自 PHP 8.0 支持
基础概念
打包与展开
可变参数和参数展开是 ... 运算符的两个方向:
函数调用 → 函数定义
...$array(展开) ←→ ...$params(收集)
调用 func(1, 2, 3) → function sum(int ...$params) → $params = [1, 2, 3]
展开 func(...$arr) → function sum(int $a, $b, $c) → $a=1, $b=2, $c=3| 操作 | 方向 | 语法 | 示例 |
|---|---|---|---|
| 收集(可变参数) | 调用 → 定义 | func(...$params) 定义中 | 多个值 → 数组 |
| 展开(参数展开) | 调用 → 定义 | func(...$array) 调用中 | 数组 → 多个值 |
语法与代码
基本数组展开
使用 ... 将数组中的元素展开为函数的独立参数。
php
<?php
declare(strict_types=1);
function add(int $a, int $b, int $c): int
{
return $a + $b + $c;
}
// 传统方式:逐个传入
echo add(1, 2, 3) . "\n"; // 6
// 使用参数展开:将数组展开为参数
$numbers = [1, 2, 3];
echo add(...$numbers) . "\n"; // 6
// 展开与固定参数混合
$nums = [2, 3];
echo add(1, ...$nums) . "\n"; // 6
// 动态构建参数
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
$batch1 = [1, 2, 3];
$batch2 = [4, 5, 6];
// 合并多个数组后展开
$all = array_merge($batch1, $batch2);
echo sum(...$all) . "\n"; // 21
// 或者直接展开多个数组
echo sum(...$batch1, ...$batch2) . "\n"; // 21与数组函数配合使用
参数展开在数组操作中非常有用,可以方便地将数组元素传递给需要多个参数的函数。
php
<?php
declare(strict_types=1);
// array_push 的替代方案
$stack = ['a', 'b', 'c'];
$newItems = ['d', 'e', 'f'];
// 传统方式
array_push($stack, 'd', 'e', 'f');
// 展开方式
array_push($stack, ...$newItems);
print_r($stack);
// Array ( [0] => a [1] => b [2] => c [3] => d [4] => e [5] => f )
// max/min 函数
$values = [3, 7, 1, 9, 4, 2];
echo max(...$values) . "\n"; // 9
echo min(...$values) . "\n"; // 1
// sprintf
$tplArgs = ['Alice', 30, 'developer'];
echo sprintf('Name: %s, Age: %d, Role: %s', ...$tplArgs) . "\n";
// Name: Alice, Age: 30, Role: developer
// in_array
$allowedRoles = ['admin', 'editor', 'viewer'];
$role = 'editor';
echo in_array($role, ...$allowedRoles) ? 'yes' : 'no';
// 注意:in_array 接受 (needle, haystack),这里展开不适用
// 正确用法是直接传数组:in_array($role, $allowedRoles)PHP 8.0+ 命名参数后展开
PHP 8.0 允许在展开数组后继续使用命名参数,这为参数覆盖和选项合并提供了便利。
php
<?php
declare(strict_types=1);
function configure(
string $driver = 'mysql',
string $host = 'localhost',
int $port = 3306,
string $database = 'test',
string $charset = 'utf8mb4'
): string {
return "driver={$driver};host={$host};port={$port};database={$database};charset={$charset}";
}
// 默认配置
$defaults = [
'driver' => 'mysql',
'host' => 'localhost',
'port' => 3306,
];
// 用户自定义配置覆盖默认值
$userConfig = [
'host' => '192.168.1.100',
'port' => 5432,
'database' => 'myapp',
];
// 合并配置(用户配置覆盖默认值)
$finalConfig = array_merge($defaults, $userConfig);
echo configure(...$finalConfig) . "\n";
// driver=mysql;host=192.168.1.100;port=5432;database=myapp;charset=utf8mb4
// 展开后再用命名参数覆盖
echo configure(...$defaults, database: 'production') . "\n";
// driver=mysql;host=localhost;port=3306;database=production;charset=utf8mb4与 Traversable 配合
... 运算符不仅支持数组,还支持实现了 Traversable 接口的对象(如 Generator、ArrayIterator 等)。
php
<?php
declare(strict_types=1);
function multiply(int $a, int $b, int $c): int
{
return $a * $b * $c;
}
// 使用 ArrayIterator
$iterator = new ArrayIterator([2, 3, 4]);
echo multiply(...$iterator) . "\n"; // 24
// 使用 Generator
function numberGenerator(): Generator
{
yield 5;
yield 6;
yield 7;
}
$gen = numberGenerator();
echo multiply(...$gen) . "\n"; // 210
// 实用示例:从数据库结果集展开参数
class UserList implements IteratorAggregate
{
private array $users;
public function __construct(array $users)
{
$this->users = $users;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->users);
}
}
function sendBatch(array ...$userBatches): int
{
$count = 0;
foreach ($userBatches as $batch) {
$count += count($batch);
}
return $count;
}
$userList = new UserList([['id' => 1], ['id' => 2], ['id' => 3]]);
echo sendBatch(...$userList) . "\n"; // 3详细说明
展开多维数组
... 运算符只展开一层。对于多维数组,每个元素会被当作一个独立参数传递。
php
<?php
declare(strict_types=1);
function processRow(string $name, int $age, string $city): string
{
return "{$name}, {$age} years old, from {$city}";
}
// 一维数组的每个元素对应一个参数
$row = ['Alice', 30, 'Beijing'];
echo processRow(...$row) . "\n"; // Alice, 30 years old, from Beijing
// 二维数组的情况
$multiRows = [
['Alice', 30, 'Beijing'],
['Bob', 25, 'Shanghai'],
];
// 展开二维数组:每个子数组作为一个参数
// 相当于 processRow(['Alice', 30, 'Beijing'], ['Bob', 25, 'Shanghai'])
// 这会报错,因为参数类型不匹配
// 正确做法:遍历展开
foreach ($multiRows as $row) {
echo processRow(...$row) . "\n";
}
// Alice, 30 years old, from Beijing
// Bob, 25 years old, from Shanghai展开空数组
展开空数组等同于不传任何参数(对于可变参数部分)。
php
<?php
declare(strict_types=1);
function greet(string $name, string ...$titles): string
{
$prefix = !empty($titles) ? implode(' ', $titles) . ' ' : '';
return "{$prefix}{$name}";
}
echo greet('Alice', ...['Dr.', 'Prof.']) . "\n"; // Dr. Prof. Alice
$emptyTitles = [];
echo greet('Bob', ...$emptyTitles) . "\n"; // Bob展开与类型安全
展开的值仍然会进行类型检查,类型不匹配时抛出 TypeError。
php
<?php
declare(strict_types=1);
function calculate(int $a, int $b): int
{
return $a + $b;
}
$values = [1, 2];
echo calculate(...$values) . "\n"; // 3
// 类型不匹配
$wrongValues = [1, 'two'];
try {
calculate(...$wrongValues); // TypeError in strict mode
} catch (TypeError $e) {
echo $e->getMessage() . "\n";
}实战示例
函数代理/装饰器模式
php
<?php
declare(strict_types=1);
// 使用参数展开创建通用代理
function timeIt(callable $func, mixed ...$args): mixed
{
$start = microtime(true);
$result = $func(...$args);
$elapsed = microtime(true) - $start;
echo "耗时: " . number_format($elapsed * 1000, 2) . " ms\n";
return $result;
}
// 被代理的函数
function heavyCalculation(int $a, int $b, int $c): int
{
usleep(100000); // 模拟耗时操作
return $a * $b + $c;
}
// 通过展开参数调用
$result = timeIt('heavyCalculation', 10, 20, 30);
echo "结果: {$result}\n"; // 结果: 230多配置合并系统
php
<?php
declare(strict_types=1);
function createAppConfig(
string $env = 'production',
bool $debug = false,
string $cacheDriver = 'redis',
int $cacheTtl = 3600,
string $logLevel = 'warning',
array $allowedOrigins = ['*']
): array {
return get_defined_vars();
}
// 基础配置
$baseConfig = [
'env' => 'development',
'debug' => true,
'logLevel' => 'debug',
];
// 环境特定配置
$devOverrides = [
'cacheDriver' => 'file',
'cacheTtl' => 60,
];
// 合并并展开
$final = array_merge($baseConfig, $devOverrides);
$config = createAppConfig(...$final);
print_r($config);中间件链式调用
php
<?php
declare(strict_types=1);
function applyMiddleware(callable $handler, callable ...$middlewares): callable
{
$pipeline = array_reduce(
array_reverse($middlewares),
fn(callable $next, callable $middleware) => fn(...$args) => $middleware($next, ...$args),
$handler
);
return $pipeline;
}
// 定义中间件
$authMiddleware = fn(callable $next, string $request) => $next("[AUTH] {$request}");
$logMiddleware = fn(callable $next, string $request) => $next("[LOG] {$request}");
// 定义最终处理器
$finalHandler = fn(string $request) => "Processed: {$request}";
// 组装中间件链
$handler = applyMiddleware($finalHandler, $authMiddleware, $logMiddleware);
echo $handler('GET /api/users') . "\n";
// Processed: [AUTH] [LOG] GET /api/users注意事项
常见陷阱
- 参数数量不匹配:展开后的参数数量必须满足函数的要求
php
<?php
declare(strict_types=1);
function needThree(int $a, int $b, int $c): int
{
return $a + $b + $c;
}
$twoItems = [1, 2];
// needThree(...$twoItems); // ArgumentCountError: Too few arguments
$fiveItems = [1, 2, 3, 4, 5];
// needThree(...$fiveItems); // ArgumentCountError: Too many arguments- 字符串键名的数组:PHP 8.0+ 中,展开关联数组时使用键名作为命名参数
php
<?php
declare(strict_types=1);
function example(int $x, int $y): int
{
return $x + $y;
}
// 数字键:按位置展开
$positional = [1, 2];
echo example(...$positional) . "\n"; // 3
// 字符串键:按命名参数展开(PHP 8.0+)
$named = ['x' => 10, 'y' => 20];
echo example(...$named) . "\n"; // 30
// 混合键:字符串键被当作命名参数
$mixed = [0 => 1, 'y' => 20];
echo example(...$mixed) . "\n"; // $x=1, $y=20, 结果: 21- Generator 只能遍历一次:展开 Generator 后,Generator 会被消耗,不能再次展开
php
<?php
declare(strict_types=1);
function show(int ...$nums): void
{
echo implode(', ', $nums) . "\n";
}
function gen(): Generator
{
yield 1;
yield 2;
yield 3;
}
$g = gen();
show(...$g); // 1, 2, 3
// show(...$g); // 空输出,Generator 已被消耗展开顺序与覆盖
php
<?php
declare(strict_types=1);
function test(int $a, int $b): int
{
return $a + $b;
}
$defaults = ['a' => 1, 'b' => 2];
$overrides = ['b' => 5];
// 展开后命名参数覆盖
echo test(...$defaults, ...$overrides) . "\n"; // 6 (a=1, b=5)
// 展开后直接指定命名参数
echo test(...$defaults, b: 10) . "\n"; // 11 (a=1, b=10)
// 同一参数不能多次指定
// echo test(a: 1, a: 2); // Error: Named parameter $a overwrites previous argument最佳实践
1. 使用展开替代 call_user_func_array
php
<?php
declare(strict_types=1);
function process(int $a, string $b, float $c): string
{
return "a={$a}, b={$b}, c={$c}";
}
$args = [42, 'hello', 3.14];
// 不推荐:旧的 call_user_func_array
echo call_user_func_array('process', $args) . "\n";
// 推荐:使用参数展开
echo process(...$args) . "\n";2. 配置合并使用命名参数展开
php
<?php
declare(strict_types=1);
// 推荐:用命名参数展开传递配置
function dbConnect(
string $host = 'localhost',
int $port = 3306,
string $database = 'app'
): string {
return "mysql://{$host}:{$port}/{$database}";
}
$config = ['host' => 'db.example.com', 'port' => 5432, 'database' => 'myapp'];
echo dbConnect(...$config) . "\n";3. 注意数组键名的含义
php
<?php
declare(strict_types=1);
// 当需要位置参数时,确保数组是数字索引的
function insert(int $id, string $name): void
{
echo "Insert #{$id}: {$name}\n";
}
// 正确:数字索引
$insertData = [1, 'Alice'];
insert(...$insertData);
// 如果不确定数组结构,可以用 array_values() 重置键名
$dataWithKeys = ['id' => 2, 'name' => 'Bob'];
insert(...array_values($dataWithKeys));