展开运算符 ...
概述
展开运算符 ...(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)
最佳实践
- 使用
...解构简化代码:[$a, $b, ...$rest] = $array替代list() - 参数展开提高灵活性:
foo(...$args)替代call_user_func_array() - 配置合并使用
...:PHP 8.1+ 中[...$defaults, ...$userConfig]更直观 - 可变参数用于包装函数:转发参数给底层函数
- 注意版本兼容性:PHP 7.4 项目不要使用字符串键展开