Skip to content

可变参数

概述

可变参数(Variadic Functions)允许函数接受任意数量的参数。PHP 5.6 引入了 ... 运算符来声明可变参数,取代了传统的 func_get_args() 系列函数。可变参数使函数更加灵活,能够处理不确定数量的输入。

PHP 版本说明

  • ... 运算符自 PHP 5.6 引入
  • func_get_args() / func_num_args() / func_get_arg() 自 PHP 8.0 起与可变参数类型声明不兼容,不推荐使用
  • 可变参数可以添加类型声明,自 PHP 7.0 起支持标量类型

基础概念

什么是可变参数

可变参数函数可以接受零个或多个参数。在函数定义中,可变参数使用 ... 前缀标识,PHP 会将传入的所有额外参数收集为一个数组。

php
function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

echo sum(1, 2, 3);    // 6
echo sum(1, 2, 3, 4); // 10
echo sum();            // 0

新旧方式对比

特性... 运算符(推荐)func_get_args()(旧式)
引入版本PHP 5.6+PHP 4+
语法...$paramsfunc_get_args()
类型声明支持不支持
与严格模式兼容否(有警告)
可读性
性能更优略差

语法与代码

基本 ... 运算符语法

php
<?php

declare(strict_types=1);

// 基本可变参数
function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

echo sum(1, 2, 3, 4, 5) . "\n";    // 15
echo sum(10, 20) . "\n";            // 30
echo sum() . "\n";                   // 0

// 可变参数在函数内部就是数组
function showNumbers(int ...$numbers): void
{
    echo "参数个数: " . count($numbers) . "\n";
    echo "参数列表: " . implode(', ', $numbers) . "\n";
}

showNumbers(1, 2, 3);
// 参数个数: 3
// 参数列表: 1, 2, 3

可变参数与固定参数混合

可变参数可以与固定参数一起使用,但可变参数必须放在参数列表的最后。

php
<?php

declare(strict_types=1);

// 固定参数 + 可变参数
function format(string $template, string ...$values): string
{
    $i = 0;
    return preg_replace_callback(
        '/\{(\d+)\}/',
        fn($match) => $values[$match[1]] ?? $match[0],
        $template
    );
}

echo format('Hello {0}, you have {1} new messages', 'Alice', 3) . "\n";
// Hello Alice, you have 3 new messages

// 多个固定参数 + 可变参数
function buildUrl(
    string $baseUrl,
    string $path,
    array $query = [],
    string ...$segments
): string {
    $url = rtrim($baseUrl, '/') . '/' . ltrim($path, '/');

    if (!empty($segments)) {
        $url .= '/' . implode('/', array_map('rawurlencode', $segments));
    }

    if (!empty($query)) {
        $queryString = http_build_query($query);
        $url .= '?' . $queryString;
    }

    return $url;
}

echo buildUrl(
    'https://api.example.com',
    'search',
    ['q' => 'php', 'page' => 1],
    'tutorials', 'variadic'
) . "\n";
// https://api.example.com/search/tutorials/variadic?q=php&page=1

位置限制

可变参数必须是参数列表的最后一个参数。以下定义会导致语法错误:

php
// 错误
function test(int ...$numbers, string $name) {}

类型声明 + 可变参数

为可变参数添加类型声明后,传入的每个参数都必须符合该类型。

php
<?php

declare(strict_types=1);

// 所有参数都必须是 int 类型
function sumInts(int ...$numbers): int
{
    return array_sum($numbers);
}

echo sumInts(1, 2, 3) . "\n"; // 6
// sumInts(1, '2', 3);         // TypeError in strict mode

// 所有参数都必须是 string 类型
function joinStrings(string ...$parts): string
{
    return implode(' - ', $parts);
}

echo joinStrings('PHP', 'MySQL', 'Redis') . "\n"; // PHP - MySQL - Redis

// 使用联合类型的可变参数
function processValues(int|float|string ...$values): array
{
    return array_map(fn($v) => get_debug_type($v) . ':' . $v, $values);
}

print_r(processValues(1, 2.5, 'hello', 42));
// Array ( [0] => int:1 [1] => double:2.5 [2] => string:hello [3] => int:42 )

旧方式:func_get_args 系列函数

在 PHP 5.6 之前,使用 func_get_args()func_num_args()func_get_arg() 获取可变参数。这些函数在现代 PHP 中仍然可用,但不推荐使用。

php
<?php

declare(strict_types=1);

// 旧方式(不推荐,在 PHP 8.0+ 中会产生编译警告)
function oldSum()
{
    $args = func_get_args();   // 获取所有参数的数组
    $count = func_num_args();  // 获取参数个数
    $first = func_get_arg(0);  // 获取指定位置的参数

    $total = 0;
    for ($i = 0; $i < $count; $i++) {
        $total += func_get_arg($i);
    }

    echo "Count: {$count}, First: {$first}, Total: {$total}\n";
}

oldSum(1, 2, 3, 4, 5);
// Count: 5, First: 1, Total: 15

// 新方式(推荐)
function newSum(int ...$numbers): int
{
    echo "Count: " . count($numbers) . "\n";
    echo "First: " . ($numbers[0] ?? 'none') . "\n";

    return array_sum($numbers);
}

newSum(1, 2, 3, 4, 5);
// Count: 5, First: 1, 返回: 15

PHP 8.0+ 兼容性

在 PHP 8.0+ 中,如果函数使用了可变参数 ...$params,则在同一函数中调用 func_get_args() 等函数会产生编译错误。两者不能在同一函数中混用。

详细说明

可变参数的内部机制

当使用 ... 运算符时,PHP 会将传入的参数打包为一个数组。这个过程是透明的,不会引入额外的性能开销。

php
<?php

declare(strict_types=1);

function analyze(int ...$numbers): void
{
    echo "类型: " . gettype($numbers) . "\n";  // array
    echo "是否数组: " . (is_array($numbers) ? '是' : '否') . "\n"; // 是
    echo "元素数量: " . count($numbers) . "\n";

    // 可以像普通数组一样操作
    echo "第一个: " . $numbers[0] . "\n";
    echo "最后一个: " . end($numbers) . "\n";
    echo "最大值: " . max($numbers) . "\n";
    echo "最小值: " . min($numbers) . "\n";
    echo "平均值: " . (array_sum($numbers) / count($numbers)) . "\n";
}

analyze(10, 20, 30, 40, 50);

可变参数的空值情况

当不传入任何参数时,可变参数变量为空数组,而不是 null

php
<?php

declare(strict_types=1);

function handleVariadic(string ...$items): void
{
    if (empty($items)) {
        echo "没有传入任何参数\n";
        return;
    }

    foreach ($items as $item) {
        echo "- {$item}\n";
    }
}

handleVariadic();                    // 没有传入任何参数
handleVariadic('item1', 'item2');    // - item1 \n - item2

可变参数与引用传递

从 PHP 5.6 开始,可变参数可以声明为引用传递(PHP 8.0+ 更推荐使用不可变数据结构)。

php
<?php

declare(strict_types=1);

function modifyAll(int &...$numbers): void
{
    for ($i = 0; $i < count($numbers); $i++) {
        $numbers[$i] *= 2;
    }
}

$values = [1, 2, 3, 4];
modifyAll(...$values);
print_r($values);
// Array ( [0] => 2 [1] => 4 [2] => 6 [3] => 8 )

引用可变参数

引用可变参数要求使用参数展开(...)来调用,不能直接传递值。如果数组中的元素是字面量,将会报错。

实战示例

通用数据聚合器

php
<?php

declare(strict_types=1);

function aggregate(string $operation, int|float ...$numbers): float
{
    return match ($operation) {
        'sum' => array_sum($numbers),
        'avg' => count($numbers) > 0 ? array_sum($numbers) / count($numbers) : 0.0,
        'max' => max($numbers),
        'min' => min($numbers),
        'product' => array_product($numbers),
        default => throw new InvalidArgumentException("Unknown operation: {$operation}"),
    };
}

echo aggregate('sum', 1, 2, 3, 4, 5) . "\n";      // 15
echo aggregate('avg', 10, 20, 30) . "\n";           // 20
echo aggregate('max', 3, 7, 1, 9, 4) . "\n";        // 9
echo aggregate('product', 2, 3, 4) . "\n";         // 24

日志记录器

php
<?php

declare(strict_types=1);

function logMessage(string $level, string $message, mixed ...$context): void
{
    $timestamp = date('Y-m-d H:i:s');
    $prefix = strtoupper($level);

    $line = "[{$timestamp}] [{$prefix}] {$message}";

    if (!empty($context)) {
        $line .= ' | Context: ' . json_encode($context, JSON_UNESCAPED_UNICODE);
    }

    echo $line . "\n";
}

logMessage('info', 'Application started');
// [2024-01-15 10:00:00] [INFO] Application started

logMessage('error', 'Database connection failed', [
    'host' => 'localhost',
    'port' => 3306,
    'error' => 'Connection refused',
]);
// [2024-01-15 10:00:00] [ERROR] Database connection failed | Context: {"host":"localhost","port":3306,"error":"Connection refused"}

批量数据处理管道

php
<?php

declare(strict_types=1);

function pipeline(mixed $data, callable ...$stages): mixed
{
    foreach ($stages as $stage) {
        $data = $stage($data);
    }

    return $data;
}

// 定义处理阶段
$trimStage = fn(string $s) => trim($s);
$lowerStage = fn(string $s) => strtolower($s);
$slugStage = fn(string $s) => preg_replace('/[^a-z0-9]+/', '-', $s);
$trimSlug = fn(string $s) => trim($s, '-');

// 通过管道处理数据
$title = "  Hello   WORLD!  This is a Test  ";
$slug = pipeline($title, $trimStage, $lowerStage, $slugStage, $trimSlug);
echo $slug . "\n";
// hello-world-this-is-a-test

// 数值处理管道
$numbers = [3, 1, 4, 1, 5, 9, 2, 6];
$result = pipeline(
    $numbers,
    fn(array $arr) => array_filter($arr, fn($n) => $n > 2),
    fn(array $arr) => array_values($arr),
    fn(array $arr) => array_map(fn($n) => $n * 2, $arr),
    fn(array $arr) => array_sum($arr),
);
echo $result . "\n"; // 68 (3*2 + 4*2 + 5*2 + 9*2 + 6*2)

注意事项

性能考量

可变参数在内部实现为数组打包,对于大量参数的场景,直接传递数组可能更高效。

php
<?php

declare(strict_types=1);

// 可变参数:每次调用都需要打包参数
function sumVariadic(int ...$numbers): int
{
    return array_sum($numbers);
}

// 直接接受数组:对于已知大量数据的情况更高效
function sumArray(array $numbers): int
{
    return array_sum($numbers);
}

$data = range(1, 1000);

// 推荐:数据量大时直接传递数组
echo sumArray($data) . "\n"; // 500500

// 可变参数适合参数个数较少且不确定的场景
echo sumVariadic(1, 2, 3, 4, 5) . "\n"; // 15

可变参数与默认参数

可变参数可以与有默认值的固定参数组合使用,但可变参数始终在最后。

php
<?php

declare(strict_types=1);

function buildSelect(
    string $table,
    string $alias = 't',
    string ...$columns
): string {
    $cols = !empty($columns) ? implode(', ', $columns) : '*';

    return "SELECT {$cols} FROM {$table} AS {$alias}";
}

echo buildSelect('users') . "\n";
// SELECT * FROM users AS t

echo buildSelect('users', 'u', 'id', 'name', 'email') . "\n";
// SELECT id, name, email FROM users AS u

最佳实践

1. 优先使用 ... 运算符

php
<?php

declare(strict_types=1);

// 推荐:现代可变参数
function processItems(string ...$items): void
{
    foreach ($items as $item) {
        echo "- {$item}\n";
    }
}

// 不推荐:旧方式
function processItemsOld(): void
{
    foreach (func_get_args() as $item) {
        echo "- {$item}\n";
    }
}

2. 为可变参数添加类型声明

php
<?php

declare(strict_types=1);

// 推荐:有类型声明
function calculateAverage(int|float ...$values): float
{
    return count($values) > 0 ? array_sum($values) / count($values) : 0.0;
}

// 不推荐:无类型声明
function calculateAverageNoType(...$values)
{
    return count($values) > 0 ? array_sum($values) / count($values) : 0;
}

3. 处理空参数场景

php
<?php

declare(strict_types=1);

function mergeConfigs(array ...$configs): array
{
    if (empty($configs)) {
        return [];
    }

    return array_merge(...$configs);
}

$default = ['debug' => false, 'cache' => true];
$env = ['debug' => true];
$user = ['theme' => 'dark'];

$merged = mergeConfigs($default, $env, $user);
print_r($merged);
// Array ( [debug] => true [cache] => true [theme] => dark )

4. 避免过多可变参数

如果可变参数可能非常大,建议直接传递数组,以避免参数栈溢出或性能问题。

参考链接