PHP 8.6 部分函数应用:用 ? 和 ... 预填参数,以及何时该用箭头函数
PHP 8.6 加入部分函数应用(partial function application,PFA):用 ? 和 ... 占位符把部分参数先填上,得到一个闭包。RFC 由 Larry Garfield 和 Arnaud Le Blanc 提出,以 33-0 通过,已合并进 PHP 8.6 alpha 3。目标版本 PHP 8.6,GA 定在 2026-11-19;本文写作时最新为 RC。
? 占位符:留一个参数,调用时再给
function tag(string $element, string $cssClass, string $content): string {
return "$content";
}
$alert = tag('div', 'alert', ?);
echo $alert('Your session expired.');
// Your session expired.
可以留空多个参数,位置占位符保持原有顺序:
$div = tag('div', ?, ?);
... 占位符:覆盖所有剩余参数
$div = tag('div', ...);
PHP 8.1 的一等可调用语法 strtolower(...) 会为整个函数创建闭包;8.6 保留这一行为,并允许在 ... 前面带参数。tag('div', ...) 固定第一个参数,闭包接收剩下的参数。
代表性例子:
$f = foo(1, ?, 3, 4);
// 等价于 static fn(int $b): int => foo(1, $b, 3, 4);
$f = foo(1, ?, 3, ...);
// 等价于 static fn(int $b, int $d): int => foo(1, $b, 3, $d);
命名参数和占位符混用
命名参数里也可以放占位符:
tag(element: 'div', cssClass: 'alert', content: ?);
命名占位符按出现顺序进入结果闭包的参数表,效果等于重排参数:
complex(b: 2, d: 4, c: ?, a: ?);
生成的闭包调用时先传 $c、再传 $a。写调用方代码时,这个顺序差异要记住。
全部填满再加 ...:零参闭包(thunk)
$default = expensive(3, 4, $point, ...);
// 之后
$default();
所有参数都预填,... 不再留下任何开放参数,得到的是零参闭包,用来延迟执行。
闭包继承什么
生成的闭包保留参数名、类型、默认值、返回类型和引用传递行为,反射与静态分析都能检查它。
属性只复制 #[SensitiveParameter] 和 #[NoDiscard];Deprecated、Override 以及用户自定义属性不会复制。
可选参数在后续 RFC 里被改成必填
后续的可选占位符 RFC 改了默认值:每个 ? 占位符都会成为闭包的必填参数,即使原参数本身有默认值。通过 ... 引入的参数则保留可选状态和默认值。
这条规则在管道里会咬人:像 array_filter(?, ...) 这种把参数留给管道传入的写法,调用时仍必须把该参数传进来,不能省掉,也不能依赖原函数的默认值。
可变参数
位置占位符落到可变参数部分时,会变成必填。这类占位符按可变参数名加 0 起后缀命名,例如 $nums0、$nums1,并且可以按名字调用。
参数表达式提前求值
参数在创建闭包时立即求值:
speak(?, getArg());
getArg() 在闭包创建时执行,而不是在闭包被调用时执行。这一点和箭头函数不同,后者函数体在调用时才求值。
哪些可调用能用
命名函数、方法、静态方法、闭包、可调用对象都可以。__call 和 __callStatic 也能用,按 function (...$args) 处理,生成的参数为 $args0...。
new 不支持,需要改用静态工厂方法。__get/__set 不是方法,不能被部分应用。
常量表达式
所有参数都是常量表达式时,PFA 调用可以出现在常量表达式里,例如属性默认值。
被禁的函数
和一等可调用一致:compact()、extract()、func_get_arg()、get_defined_vars() 不可用。
静态闭包与作用域
当且仅当被部分应用的函数是静态的,生成的闭包才是静态的(不可绑定)。如果函数带绑定对象,闭包只能重绑定到同一类型。后期静态绑定 static:: 正常工作。
PFA 不捕获创建处的作用域,这一点和 use 或箭头函数不同:它只预填参数。
实现方式
引擎为等价的闭包生成 AST,再编译执行。二次编译的 AST 存入 opcache,因此只发生一次。
和 PHP 8.5 管道操作符配合
管道 |> 需要接收一个参数的可调用对象,而不少常用函数接收两个或更多参数。PFA 正好补上一元闭包。
当部分调用直接位于管道右侧,且能静态证明只接收一个参数时,部分应用会被优化掉,变成直接调用,不创建闭包。可优化的形式:
foo(?)foo(1, ?)foo(1, a: ?)foo(1, ...)
不会被优化的形式:
foo(a: 1, ...)foo(?, ?)foo(?, a: ?)foo(?, ...)
例子:
$numberOfAdmins = getUsers()
|> array_filter(?, isAdmin(...))
|> count(...);
$result = "Hello World"
|> htmlentities(...)
|> str_split(...)
|> array_map(strtoupper(...), ?)
|> array_filter(?, fn($v) => $v != 'O');
回调场景下更明显:
$result = array_map(in_array(?, $legal, strict: true), $input);
// 对比手写箭头函数
$result = array_map(
fn(string $s): bool => in_array($s, $legal, strict: true),
$input
);
没有向后不兼容的改动,只是新增语法。
函数组合还没到:管道是立即执行,组合则是创建一个新函数。
另外,据 phpnews 总结,8.6 在某些情况下会把 array_map(pfa/fcc, ...) 编译成 foreach。
取舍:什么时候用 PFA,什么时候写箭头函数
适合用 PFA 的场合:
- 回调只需要固定部分参数,例如
in_array(?, $legal, strict: true),签名直接对得上,不用手写fn。 - 管道右侧需要一元函数。单参数的部分调用会被编译期消除,没有闭包创建开销。
- 需要延迟执行:把所有参数先算好,用
...收尾得到零参闭包,稍后再调用。
箭头函数更清楚的场合:
- 需要在调用时才算表达式。
speak(?, getArg())里的getArg()在闭包创建时就执行了,想要调用时求值只能用箭头函数。 - 需要捕获外层变量。PFA 只预填参数,不做捕获。
- 占位符顺序不直观时。命名占位符按出现顺序重排参数,
complex(b: 2, d: 4, c: ?, a: ?)的调用顺序是$c、$a,读代码时不如fn直白。 - 需要
new,或需要把闭包重绑定到别的类型。new不支持;静态函数的 PFA 结果是静态闭包,无法bindTo。
可变参数与命名占位符混用时还有两个坑:位置占位符一旦落进可变参数区域就变必填,参数名是 $nums0/$nums1 这类后缀形式,按名调用容易写错;命名占位符的书写顺序就是闭包的参数顺序,等于隐式重排,调用方必须按新顺序传参。