编程 PHP 8.6 的部分函数应用:`?` 与 `...` 占位符、变长规则与管道下的零开销

2026-10-25 21:03:15

PHP 8.6 的部分函数应用:? 与 ... 占位符、变长规则与管道下的零开销

项目信息

  • RFC:
  • PHP Foundation 博客:
  • php.watch 摘要:
  • 源码仓库:

Partial Function Application(PFA,部分函数应用)指只传入函数的一部分必需参数、把其余参数延后。该 RFC 由 Larry Garfield 与 Arnaud Le Blanc 提交,已合入 master(8.6 alpha3),投票结果 33-0。

逻辑上它等价于:

function f(int $a, int $b, int $c) {}
$partialF = fn(int $b) => f(1, $b, 3);

手写箭头函数的问题是必须手工复制全部参数信息,用回调或新的管道运算符时尤其啰嗦:

$result = array_map(static fn(string $string): string => str_replace('hello', 'hi', $string), $arr);

PFA 是一等可调用语法(FCC,PHP 8.1)的自然延伸:FCC 是 PFA 的退化情形。

基本语义

一次部分应用调用中,一个或多个实参被替换成 ? 或 ...。引擎创建一个 Closure,保存目标函数与已给定的实参。该闭包的签名与未指定的参数一致,因此反射能拿到参数与返回值的完整类型信息。

给定 function foo(int $a, int $b, int $c, int $d): int:

$f = foo(1, ?, 3, 4);      // fn(int $b): int => foo(1, $b, 3, 4)
$f = foo(1, ?, 3, ?);      // fn(int $b, int $d): int => foo(1, $b, 3, $d)
$f = foo(1, ...);          // fn(int $b, int $c, int $d): int => foo(1, $b, $c, $d)
$f = foo(1, 2, ...);       // fn(int $c, int $d): int => foo(1, 2, $c, $d)
$f = foo(1, ?, 3, ...);    // fn(int $b, int $d): int => foo(1, $b, 3, $d)

两种占位符语义:

  • ? 表示该位置恰好接收一个参数。
  • ... 表示该位置接收零个或多个参数。

语法顺序为:零个或多个位置值(字面量或变量),与 ? 混排;零个或多个命名参数,带值或带占位符;零个或一个 ... 符号。

生成的闭包继承底层函数的:名称、类型、可选性、默认值、按引用传参/按引用返回,以及 #[NoDiscard] 和 #[SensitiveParameter] 两个属性是否存在(仅这两个)。#[Deprecated] 会导致重复警告,#[Override] 用在闭包上没有意义,用户自定义属性被忽略。

变长参数的三条附加规则

  • 任何落入变长参数区的位置占位符都变成必需参数。
  • 只要有位置占位符落入变长参数区,此前所有剩余的占位符也都变成必需参数。
  • 映射到变长部分的占位符,按变长参数名加 0 起始的数字后缀命名:int ...$nums → $nums0、$nums1,可以按名调用。
function foo(int $a = 5, int $b = 1, string ...$c) { }
$pfa = foo(?, ?, ?, ?);
// 等价于:$a 和 $b 变成必需参数,因为至少要提供 4 个实参
$pfa = static fn(int $a, int $b, string $c0, string $c1) => foo($a, $b, $c0, $c1);

命名实参与位置占位符混用

带值的命名实参按名字映射到对应参数,与书写顺序无关。用 ? 的命名实参则按出现顺序放入闭包,相当于对参数重新排序。位置占位符与 ... 仍遵循原函数的参数顺序。

function stuff(int $i, string $s, float $f, Point $p, int $m = 0) {}
$c = stuff(1, ?, p: ?, f: 3.14, ...);
// 等价于:$c = static fn(string $s, Point $p, int $m = 0) => stuff(1, $s, 3.14, $p, $m);

在没有剩余参数时使用 ... 是合法的,会生成一个没有必需参数的闭包,用已给的值调用函数,即 thunk。

三种预期用法

// 只留一个参数不填
$c = array_map(strtoupper(...), ?);
$c = array_filter(?, is_numeric(...));

// 先给一两个值,剩下的"留空"
$c = stuff(1, 'two', ...);

// 按名填几个参数,其余保持原样
$c = stuff(f: 3.14, s: 'two', ...);

错误示例

仍以 stuff(int $i, string $s, float $f, Point $p, int $m = 0) 为例:

$c = stuff(?);                          // ArgumentCountError: expects at least 4 arguments, 1 given
$c = stuff(?, ?, ?, ?, ?, ?);           // ArgumentCountError: expects at most 5 arguments, 6 given
$c = stuff(?, ?, 3.5, $point, i: 5);    // Error: Named parameter $i overwrites previous placeholder
$c = stuff(i: 1, ?, ?, ?, ?);           // Fatal error: Cannot use positional placeholder after named argument
$c = stuff(?, ?, ?, p: $point, ?);      // 同上一条 fatal error

不兼容的函数

与 FCC 相同:compact()、extract()、func_get_arg()、get_defined_vars()。这些函数依赖特定上下文,而 PFA 改变了该上下文。

魔术方法 __call / __callStatic 支持,按 function (...$args) 处理;生成的必需参数无类型,名为 $args0、$args1……

$f = new Foo(); $m = $f->method(?, ?);
$m(1, 2);                // 输出 args [0 => 1, 1 => 2]
$m(args0: 1, args1: 2);  // 带名字的 args
$m(a: 1, b: 2);          // Error: Unknown named parameter $a

构造函数不支持

new 调用不能做部分应用。构造函数由引擎间接调用;而且在 PHP 8.4 引入惰性对象之后,可用的场景也少了很多。静态工厂方法则完全支持。

实现方式

这件事通常无法在编译期完成。引擎生成一个等价闭包的 AST,再编译并执行它;二次编译出来的 AST 会存进 opcache,所以只发生一次。

$f = foo(?, 3, ?);
// 转译成
$f = static function (int $a, int $c): string { return foo($a, 3, $c); };

管道运算符下可零开销

如果一个 PFA 调用直接位于管道运算符右侧,并且能静态判定它只接收一个参数,那么部分应用会被优化成直接调用,与一等可调用一样。

  • 可优化:foo(?)、foo(1, ?)、foo(1, a: ?)、foo(1, ...)
  • 不可优化:foo(a: 1, ...)、foo(?, ?)、foo(?, a: ?)、foo(?, ...)
$numberOfAdmins = getUsers() |> array_filter(?, isAdmin(...)) |> count(...);

管道相关的优化意味着这些情况下闭包根本不必创建,因此没有性能开销。

作用域

与 FCC 一致:当且仅当被部分应用的函数是静态的,PFA 闭包才是静态的(不可绑定)。如果它绑定了对象,闭包可以重新绑定,但只能绑到相同类型。

常见用例

可调用引用;一元函数(回调和管道右侧):

$result = array_map(in_array(?, $legal, strict: true), $input);

延迟执行(thunk):

$default = expensive(3, 4, $point, ...);  // 闭包,尚未调用
if ($some_condition) { $result = $default(); }

反射:结果是普通 Closure,不需要改动反射 API(ReflectionFunction 即可)。

PHP Foundation 博客(Larry Garfield,2025-12-08)里的两个例子:

$underscore = str_replace(' ', '_', ?);
// 实际就是 fn(string $s): string => str_replace(' ', '_', $s);
// 两者生成的 opcode 几乎一致

$f = complex(b: 2, d: 4, c: ?, a: ?);  // 顺序反转,$f 先要 $c 再要 $a
$f = complex(1, 2, 3, 4, ...);         // 零参数闭包

向后不兼容变更

没有,只新增语法。

历史

PFA 最早由 Joe Watkins、Levi Morrison、Paul Crovella、Larry Garfield 于 2021 年提出;Nikita Popov 认为它给引擎引入了过多复杂度,最终 PHP 8.1 先落地的是 FCC(foo(...))。第二版借助 FCC 的工作,直接创建一个普通闭包对象。

复制全文 生成海报 PHP 闭包 RFC 语法特性

推荐文章

程序员茄子在线接单