Skip to content

数组解包 ...$arr

概述

PHP 7.4+ 引入了数组解包运算符 ...$arr(也称为展开运算符或 splat operator)。它可以在函数调用中和数组字面量中展开数组参数。本章详细讲解 ... 运算符的用法以及与 array_merge 的区别。

基础概念

用法版本语法说明
函数调用展开PHP 5.6+func(...$arr)将数组元素作为参数传入
数组字面量展开PHP 7.4+[...$arr]合并数组
参数解包PHP 5.6+function func(...$args)可变参数

PHP 7.4+

数组字面量中的展开运算符 ... 需要 PHP 7.4+。函数调用中的展开 PHP 5.6+ 就支持。

语法与代码

函数调用中展开(PHP 5.6+)

php
<?php

declare(strict_types=1);

function add(int $a, int $b, int $c): int
{
    return $a + $b + $c;
}

// 展开数组为函数参数
$numbers = [1, 2, 3];
echo add(...$numbers);  // 6

// 等同于 add(1, 2, 3)

// 实际应用
$fruits = ['apple', 'banana', 'cherry'];
echo implode(', ', $fruits);         // 常规方式
echo implode(', ', [...$fruits]);    // 展开方式(效果相同)

// 与其他参数组合
function greet(string $prefix, string ...$names): string
{
    return $prefix . ': ' . implode(', ', $names);
}

$nameList = ['Alice', 'Bob', 'Charlie'];
echo greet('Hello', ...$nameList);
// Hello: Alice, Bob, Charlie

数组字面量中展开(PHP 7.4+)

php
<?php

declare(strict_types=1);

// 在数组字面量中使用展开运算符合并数组
$arr1 = [1, 2, 3];
$arr2 = [4, 5, 6];
$merged = [...$arr1, ...$arr2];
// [1, 2, 3, 4, 5, 6]

// 与其他元素混合
$combined = ['start', ...$arr1, 'middle', ...$arr2, 'end'];
// ['start', 1, 2, 3, 'middle', 4, 5, 6, 'end']

// 解包生成器
$gen = (function () {
    yield 1;
    yield 2;
    yield 3;
})();

$arr = [...$gen];
// [1, 2, 3]

与 array_merge 的区别

php
<?php

declare(strict_types=1);

// 关键区别:数字键的行为不同
$arr1 = [0 => 'a', 1 => 'b'];
$arr2 = [0 => 'c', 1 => 'd'];

// array_merge - 数字键重建索引
$merged = array_merge($arr1, $arr2);
// ['a', 'b', 'c', 'd'](索引 0,1,2,3)

// ... 展开运算符 - 数字键保持原样(后面的覆盖前面的)
$spread = [...$arr1, ...$arr2];
// ['c', 'd'](0 => 'c' 覆盖 0 => 'a',1 => 'd' 覆盖 1 => 'b')

// 对字符串键的行为相同(后面的覆盖前面的)
$assoc1 = ['key' => 'value1'];
$assoc2 = ['key' => 'value2'];

echo array_merge($assoc1, $assoc2)['key'];  // value2
echo [...$assoc1, ...$assoc2]['key'];      // value2

// + 运算符 - 保留第一个
echo ($assoc1 + $assoc2)['key'];           // value1

展开运算符的限制

php
<?php

declare(strict_types=1);

// 只能展开 integer 键的数组
$indexed = [1, 2, 3];
$assoc = ['a' => 1, 'b' => 2];

// 正确:展开索引数组
$result = [...$indexed];  // [1, 2, 3]

// PHP 8.1+ 允许展开字符串键数组
$assoc = ['a' => 1, 'b' => 2];
$result = [...$assoc];  // PHP 8.1+: ['a' => 1, 'b' => 2]

// PHP < 8.1: 字符串键会报 Fatal Error
// PHP 8.1+: 重复键时后面的覆盖前面的
$a = ['x' => 1, 'y' => 2];
$b = ['x' => 3, 'z' => 4];
$result = [...$a, ...$b];
// ['x' => 3, 'y' => 2, 'z' => 4]

可变参数(Variadic)

php
<?php

declare(strict_types=1);

// 可变参数使用 ... 声明
function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

echo sum(1, 2, 3);        // 6
echo sum(...[1, 2, 3]);   // 6(展开数组)

// 类型安全的可变参数
function concatenate(string ...$parts): string
{
    return implode('', $parts);
}

echo concatenate('Hello', ' ', 'World');  // Hello World

// 可变参数 + 普通参数
function format(string $template, string ...$values): string
{
    $i = 0;
    return preg_replace_callback('/%s/',
        fn() => $values[$i++] ?? '',
        $template
    );
}

echo format('Hello %s, you have %s messages', 'Alice', '5');

详细说明

展开运算符的行为总结

场景... 展开array_merge+ 运算符
数字键后者覆盖前者重建索引保留前者
字符串键后者覆盖前者后者覆盖前者保留前者
性能快(单次操作)较慢(函数调用)

选择建议

需要合并索引数组并重建索引时用 array_merge。需要合并并覆盖(保持索引)时用 ...。需要保留第一个值时用 +

实战示例

中间件管道

php
<?php

declare(strict_types=1);

class Pipeline
{
    public static function through(array $middlewares, callable $final): mixed
    {
        foreach (array_reverse($middlewares) as $middleware) {
            $final = fn(): mixed => $middleware($final);
        }
        return $final();
    }
}

$middlewares = [
    fn(callable $next): mixed => $next(), // passthrough
    fn(callable $next): mixed => $next(),
];

$result = Pipeline::through($middlewares, fn(): string => 'Done');
echo $result;  // Done

函数参数转发

php
<?php

declare(strict_types=1);

class HttpClient
{
    public function get(string $url, array $options = []): string
    {
        return $this->request('GET', $url, ...$options);
    }

    public function request(string $method, string $url, string ...$options): string
    {
        // 处理请求
        return "{$method} {$url}";
    }
}

$client = new HttpClient();
echo $client->get('https://example.com', ['timeout' => '30']);

展开运算符的高级用法

php
<?php

declare(strict_types=1);

// 展开运算符与 array_values
// 当需要重建索引数组时,展开比 array_values 更语义化
$filtered = array_filter([1, 2, 3, 4, 5], fn(int $n): bool => $n > 2);
$reindexed = [...$filtered];
// [3, 4, 5]

// 展开生成器(PHP 7.4+)
function numberRange(int $start, int $end): \Generator
{
    for ($i = $start; $i <= $end; $i++) {
        yield $i;
    }
}

$arr = [...numberRange(1, 5)];
// [1, 2, 3, 4, 5]

// 合并多个集合
$set1 = new \Ds\Set([1, 2, 3]);
$set2 = new \Ds\Set([3, 4, 5]);
$combined = [...$set1->toArray(), ...$set2->toArray()];
// [1, 2, 3, 3, 4, 5](注意:Set 展开后可能有重复)

// 真正的集合并集
$union = new \Ds\Set([...$set1->toArray(), ...$set2->toArray()]);
// {1, 2, 3, 4, 5}

可变参数与展开

php
<?php

declare(strict_types=1);

// 可变参数 vs 展开数组
function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

// 直接传参
echo sum(1, 2, 3, 4, 5);  // 15

// 展开数组传参
$numbers = [1, 2, 3, 4, 5];
echo sum(...$numbers);      // 15

// 混合使用
function concat(string $sep, string ...$parts): string
{
    return implode($sep, $parts);
}

echo concat(' - ', 'Hello', 'World', 'PHP');
// Hello - World - PHP

echo concat(' - ', ...['Hello', 'World', 'PHP']);
// Hello - World - PHP

// 展开多个数组
function merge(...$arrays): array
{
    return array_merge(...$arrays);
}

echo json_encode(merge([1, 2], [3, 4], [5, 6]));
// [1, 2, 3, 4, 5, 6]

展开运算符的性能对比

php
<?php

declare(strict_types=1);

// [...] vs array_merge vs +
$n = 10000;

$arr1 = range(1, $n);
$arr2 = range($n + 1, $n * 2);

// [...] 展开
$start = microtime(true);
$result = [...$arr1, ...$arr2];
$t1 = microtime(true) - $start;

// array_merge
$start = microtime(true);
$result = array_merge($arr1, $arr2);
$t2 = microtime(true) - $start;

// + 运算符
$start = microtime(true);
$result = $arr1 + $arr2;
$t3 = microtime(true) - $start;

// + 对于索引数组不是合并而是保留第一个,结果不同
// 展开和 array_merge 的性能大致相当
echo "展开: {$t1}s\n";
echo "array_merge: {$t2}s\n";

PHP 8.1+ 字符串键展开

PHP 8.1 之前,... 只能展开整数键数组。PHP 8.1+ 支持展开字符串键数组,相同键时后者覆盖前者。

展开运算符的限制与解决方案

php
<?php

declare(strict_types=1);

// 字符串键数组展开(PHP 8.1+)
$a = ['x' => 1, 'y' => 2];
$b = ['x' => 3, 'z' => 4];

$result = [...$a, ...$b];
// ['x' => 3, 'y' => 2, 'z' => 4](重复键取后者)

// PHP < 8.1 的替代方案:使用 array_merge
$result = array_merge($a, $b);  // 同样的结果

// 注意:展开运算符展开 SplFixedArray
$fixed = new SplFixedArray(3);
$fixed[0] = 'a';
$fixed[1] = 'b';
$fixed[2] = 'c';
$arr = [...$fixed];  // PHP 8.0+: ['a', 'b', 'c']

// 展开多个可迭代对象
function mergeAll(iterable ...$iterables): array
{
    $result = [];
    foreach ($iterables as $iterable) {
        foreach ($iterable as $item) {
            $result[] = $item;
        }
    }
    return $result;
}

注意事项

PHP 8.1 前

PHP 8.0 及之前版本,... 只能展开整数键数组。展开字符串键数组会报 Fatal Error。

展开大数组

展开非常大的数组(100万+ 元素)可能导致内存问题。考虑使用生成器或逐元素处理。

最佳实践

  1. 合并索引数组用 array_merge:需要重建索引
  2. 合并关联数组用 ... 或 +:根据覆盖需求选择
  3. 函数参数展开用 ...:替代 call_user_func_array
  4. 可变参数用 ...$args:替代 func_get_args()
  5. PHP 8.1+ 关联展开:注意重复键的覆盖行为

参考链接