Skip to content

展开运算符 ...

概述

展开运算符 ...(Spread Operator)是 PHP 引入的多功能运算符,经历了多个版本的演进。它支持参数展开(PHP 5.6+)、函数调用中的参数解包(PHP 5.6+)、数组解构(PHP 7.4+)以及字符串键数组的展开(PHP 8.1+)。

版本演进

  • PHP 5.6+:参数展开(可变参数函数)
  • PHP 7.4+:数组解构赋值(使用 ... 在赋值中)
  • PHP 8.1+:字符串键数组展开(支持关联数组)

基础概念

运算符的多种用途

用途版本说明
函数参数展开PHP 5.6+function foo(...$args) 接收可变参数
参数解包PHP 5.6+foo(...$array) 将数组展开为函数参数
数组解构PHP 7.4+[$a, $b, ...$rest] = $array
数组展开(合并)PHP 7.4+(数字键)/ 8.1+(字符串键)[...$arr1, ...$arr2]
命名参数展开PHP 8.0+foo(...$args) 支持 key: value 语法

语法与代码示例

函数参数展开(可变参数)

php
<?php

declare(strict_types=1);

// 可变参数函数:接收任意数量的参数
function sum(int|float ...$numbers): int|float
{
    $total = 0;
    foreach ($numbers as $num) {
        $total += $num;
    }
    return $total;
}

echo sum(1, 2, 3) . "\n";       // 6
echo sum(10, 20, 30, 40) . "\n"; // 100
echo sum() . "\n";               // 0

// 可变参数 + 普通参数混合
function format(string $prefix, string $suffix, string ...$items): string
{
    return $prefix . implode(', ', $items) . $suffix;
}

echo format('[', ']', 'apple', 'banana', 'cherry') . "\n";
// [apple, banana, cherry]

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

echo concatenate('hello', 'world') . "\n"; // hello - world

参数解包

php
<?php

declare(strict_types=1);

// 将数组展开为函数参数
function add(int $a, int $b): int
{
    return $a + $b;
}

$params = [10, 20];
echo add(...$params) . "\n"; // 30

// 将数组展开为多个参数
function createProfile(string $name, int $age, string $city): array
{
    return compact('name', 'age', 'city');
}

$data = ['Alice', 30, 'Shanghai'];
print_r(createProfile(...$data));
// ['name' => 'Alice', 'age' => 30, 'city' => 'Shanghai']

// 展开部分参数 + 固定参数
function createUser(string $role, string $name, string $email): string
{
    return "{$role}: {$name} <{$email}>";
}

$userInfo = ['Alice', 'alice@example.com'];
echo createUser('admin', ...$userInfo) . "\n";
// admin: Alice <alice@example.com>

数组合并展开

php
<?php

declare(strict_types=1);

// PHP 7.4+:数字索引数组的展开
$arr1 = [1, 2, 3];
$arr2 = [4, 5, 6];

$merged = [...$arr1, ...$arr2];
print_r($merged); // [1, 2, 3, 4, 5, 6]

// 展开时插入新元素
$combined = [...$arr1, 99, ...$arr2];
print_r($combined); // [1, 2, 3, 99, 4, 5, 6]

// PHP 8.1+:字符串键数组(关联数组)的展开
$defaults = [
    'host' => 'localhost',
    'port' => 3306,
    'charset' => 'utf8mb4',
];

$userConfig = [
    'host' => '192.168.1.100',
    'timeout' => 30,
];

// 字符串键冲突时,后出现的覆盖先出现的
$mergedConfig = [...$defaults, ...$userConfig];
print_r($mergedConfig);
// ['host' => '192.168.1.100', 'port' => 3306, 'charset' => 'utf8mb4', 'timeout' => 30]

// 插入/覆盖单个值
$final = [...$defaults, 'port' => 5432, ...$userConfig];
print_r($final);
// ['host' => '192.168.1.100', 'port' => 5432, 'timeout' => 30, 'charset' => 'utf8mb4']

PHP 版本差异

PHP 7.4 仅支持数字键数组的展开。PHP 7.4 中展开字符串键数组会抛出 Error。PHP 8.1+ 才支持字符串键数组展开。

数组解构赋值

php
<?php

declare(strict_types=1);

// PHP 7.1+ 基本数组解构
$coordinates = [10, 20, 30];
[$x, $y, $z] = $coordinates;
var_dump($x, $y, $z); // int(10), int(20), int(30)

// PHP 7.4+ 使用 ... 收集剩余元素
$numbers = [1, 2, 3, 4, 5];
[$first, $second, ...$rest] = $numbers;
var_dump($first);  // int(1)
var_dump($second); // int(2)
var_dump($rest);   // array(3) [3, 4, 5]

// 跳过元素
[, $second, , $fourth] = [10, 20, 30, 40];
var_dump($second); // int(20)
var_dump($fourth); // int(40)

// 交换变量
$a = 1;
$b = 2;
[$a, $b] = [$b, $a];
var_dump($a, $b); // int(2), int(1)

// 关联数组解构(PHP 7.1+)
$user = ['name' => 'Alice', 'age' => 30, 'email' => 'alice@example.com'];
['name' => $name, 'age' => $age] = $user;
var_dump($name, $age); // string(5) "Alice", int(30)

// 带默认值的解构
$config = ['debug' => true];
['debug' => $debug, 'cache' => $cache] = [...$config, 'cache' => false];
var_dump($debug, $cache); // bool(true), bool(false)

PHP 8.0+ 命名参数展开

php
<?php

declare(strict_types=1);

function setDatabase(
    string $host = 'localhost',
    int $port = 3306,
    string $charset = 'utf8mb4'
): string {
    return "mysql://{$host}:{$port}?charset={$charset}";
}

// 使用命名参数展开
$defaults = [
    'host' => 'localhost',
    'port' => 3306,
    'charset' => 'utf8mb4',
];

echo setDatabase(...$defaults) . "\n";
// mysql://localhost:3306?charset=utf8mb4

// 覆盖部分命名参数
$override = [
    'host' => '192.168.1.100',
    'port' => 5432,
];

echo setDatabase(...$override) . "\n";
// mysql://192.168.1.100:5432?charset=utf8mb4(charset 使用默认值)

// 展开与直接命名参数混合
echo setDatabase(
    host: '10.0.0.1',
    ...['port' => 5432],
    charset: 'latin1'
) . "\n";
// mysql://10.0.0.1:5432?charset=latin1

详细说明

展开运算符的性能考虑

php
<?php

declare(strict_types=1);

// 展开运算符创建新数组,不是引用
$a = [1, 2, 3];
$b = [...$a];
$b[] = 4;

var_dump($a); // [1, 2, 3] —— $a 不受影响
var_dump($b); // [1, 2, 3, 4]

// 对于大型数组,展开运算符会有内存和性能开销
// 因为需要复制整个数组
$large = range(1, 100000);
$start = microtime(true);
$copy = [...$large];
$elapsed = microtime(true) - $start;
echo "Copy time: {$elapsed}s\n";

// 如果只是读取,不需要展开
// 使用引用或直接传递

展开与数组函数的对比

php
<?php

declare(strict_types=1);

// 展开运算符 vs array_merge
$a = [1, 2, 3];
$b = [4, 5, 6];

// 展开运算符
$result1 = [...$a, ...$b];

// array_merge
$result2 = array_merge($a, $b);

var_dump($result1 === $result2); // bool(true) —— 对于数字键相同

// 字符串键差异
$defaults = ['a' => 1, 'b' => 2];
$override = ['b' => 3, 'c' => 4];

$spread = [...$defaults, ...$override]; // PHP 8.1+
$merged = array_merge($defaults, $override);

print_r($spread);
// ['a' => 1, 'b' => 3, 'c' => 4]

print_r($merged);
// ['a' => 1, 'b' => 3, 'c' => 4]

// 对于数字键重索引的差异
$x = [0 => 'a', 1 => 'b'];
$y = [0 => 'c', 1 => 'd'];

$spreadResult = [...$x, ...$y]; // [0 => 'a', 1 => 'b', 0 => 'c', 1 => 'd] —— 键冲突!
$mergeResult = array_merge($x, $y); // [0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd] —— 重新索引

数字键差异

展开运算符在数字键冲突时会保留两个值(后者覆盖前者),而 array_merge 会重新索引。对于不重叠的数字键数组,两者效果相同。

实战示例

中间件链

php
<?php

declare(strict_types=1);

interface Middleware
{
    public function handle(Request $request, Closure $next): Response;
}

class Request
{
    public function __construct(public string $path) {}
}

class Response
{
    public function __construct(public int $status, public string $body) {}
}

class AuthMiddleware implements Middleware
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->path === '/admin') {
            return new Response(401, 'Unauthorized');
        }
        return $next($request);
    }
}

class CorsMiddleware implements Middleware
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }
}

// 使用展开运算符构建中间件栈
function createPipeline(Middleware ...$middlewares): Closure
{
    return function (Request $request) use ($middlewares): Response {
        $next = fn(Request $req): Response => new Response(200, 'OK');

        // 从最后一个中间件开始向前包装
        for ($i = count($middlewares) - 1; $i >= 0; $i--) {
            $middleware = $middlewares[$i];
            $next = fn(Request $req): Response => $middleware->handle($req, $next);
        }

        return $next($request);
    };
}

$middlewares = [
    new CorsMiddleware(),
    new AuthMiddleware(),
];

$pipeline = createPipeline(...$middlewares);
$response = $pipeline(new Request('/admin'));
var_dump($response->status); // int(401)

配置合并工具

php
<?php

declare(strict_types=1);

class ConfigMerger
{
    /**
     * 递归合并配置(PHP 8.1+)
     */
    public static function merge(array ...$configs): array
    {
        $result = [];
        foreach ($configs as $config) {
            foreach ($config as $key => $value) {
                if (
                    isset($result[$key])
                    && is_array($result[$key])
                    && is_array($value)
                ) {
                    $result[$key] = self::merge($result[$key], $value);
                } else {
                    $result[$key] = $value;
                }
            }
        }
        return $result;
    }
}

$defaults = [
    'database' => [
        'host' => 'localhost',
        'port' => 3306,
    ],
    'cache' => [
        'enabled' => true,
        'ttl' => 3600,
    ],
];

$userConfig = [
    'database' => [
        'host' => '192.168.1.100',
    ],
    'cache' => [
        'ttl' => 7200,
    ],
];

$final = ConfigMerger::merge($defaults, $userConfig);
print_r($final);
// database: host=192.168.1.100, port=3306
// cache: enabled=true, ttl=7200

函数调用转发

php
<?php

declare(strict_types=1);

class HttpClient
{
    public function get(string $url, array $headers = []): string
    {
        return "GET {$url} with " . count($headers) . " headers";
    }

    public function post(string $url, array $data, array $headers = []): string
    {
        return "POST {$url} with data and " . count($headers) . " headers";
    }
}

// 使用可变参数 + 参数转发
class ApiClient
{
    public function __construct(private HttpClient $http) {}

    public function call(string $method, string $url, mixed ...$args): string
    {
        return match ($method) {
            'GET' => $this->http->get($url, ...$args),
            'POST' => $this->http->post($url, ...$args),
            default => throw new InvalidArgumentException("Unknown method: {$method}"),
        };
    }
}

$client = new ApiClient(new HttpClient());
echo $client->call('GET', '/api/users', ['Auth' => 'Bearer token']) . "\n";
echo $client->call('POST', '/api/users', ['name' => 'Alice'], ['Auth' => 'Bearer token']) . "\n";

注意事项

  • PHP 7.4 仅支持数字键展开:字符串键数组展开需要 PHP 8.1+
  • 展开创建副本[...$a] 创建新数组,不是引用
  • 命名参数展开(PHP 8.0+):必须使用 key => value 格式
  • 数字键冲突:展开运算符不重新索引,array_merge 会重新索引
  • 可变参数必须是最后一个function foo($a, ...$rest) 而非 function foo(...$rest, $a)

最佳实践

  1. 使用 ... 解构简化代码[$a, $b, ...$rest] = $array 替代 list()
  2. 参数展开提高灵活性foo(...$args) 替代 call_user_func_array()
  3. 配置合并使用 ...:PHP 8.1+ 中 [...$defaults, ...$userConfig] 更直观
  4. 可变参数用于包装函数:转发参数给底层函数
  5. 注意版本兼容性:PHP 7.4 项目不要使用字符串键展开

参考链接