编程 PHP 8.6 部分函数应用:用 ? 和 ... 预填参数,以及何时该用箭头函数

2026-10-27 21:32:01

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 这类后缀形式,按名调用容易写错;命名占位符的书写顺序就是闭包的参数顺序,等于隐式重排,调用方必须按新顺序传参。

复制全文 生成海报 PHP 8.6 部分函数应用 闭包 管道操作符 RFC

推荐文章

程序员茄子在线接单