Skip to content

参数展开

概述

参数展开(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

注意事项

常见陷阱

  1. 参数数量不匹配:展开后的参数数量必须满足函数的要求
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
  1. 字符串键名的数组: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
  1. 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));

参考链接