PHP 8.5: 管道操作符 (|>)
PHP 8.5 添加了一个新的操作符:管道操作符(|>
),用以从左到右链式调用多个 callable,并将左边 callable 返回的值传入到右边。
管道操作符并不会为语言添加新的能力。它以更直观的方式“链式调用”多个 callable,而不必像以前那样嵌套调用多个callable 或者使用变量获取返回值再传入下一个 callable。
$result = "Hello World"
|> strtoupper(...)
|> str_shuffle(...)
|> trim(...);
// "LWHO LDLROE"
上述代码使用了新增的管道操作符(|>),将前一个 callable 的返回值作为参数传入后一个 callable 中。 函数后面的
(...) 使之成为 first-class callable。
以下两段代码显示的是不使用管道操作符如何得到相同的结果:
使用嵌套调用
$result = trim(str_shuffle(strtoupper("Hello World")));
使用变量
$result = "Hello World";
$result = strtoupper($result);
$result = str_shuffle($result);
$result = trim($result);
写时拷贝
当使用变量来持有另外一个函数调用的返回值时,PHP 使用写时拷贝(copy-on-write),这意味着即使创建了新的变量,在变量被写之前,它不会消耗额外的系统内存。虽然管道操作符提供了更为简洁的代码书写方式,使用临时变量也未必会消耗更多的资源。
虽然使用换行可以让管道操作符代码更为清晰易读,但换行并非必需的。下面的示例代码也是完全有效合法的:
$result = strtoupper("Hello World")
管道操作符用例
管道操作符将左边函数的返回值传递给右边的函数。当 PHP 脚本编译之后,管道调用的调用链将被优化,并产生类似于嵌套调用的操作码(opcode)。
不过,管道操作符也有一些限制。当链条中所有函数都只需要一个参数、有返回值且不接受参数引用时,适合使用它。
管道操作符接受任何的 callable
管道操作符链路中的 callable 可以是任何形式的 callable。比如,可以将用户空间函数、PHP 内置函数、静态类方法、lambda 函数、箭头函数、实现 __invoke__
魔术方法的类以及 first-class callable 一起混合使用。
$result = "Hello World"
|> 'strtoupper'
|> str_shuffle(...)
|> fn($x) => trim($x)
|> function(string $x): string {return strtolower($x);}
|> new MyClass()
|> [MyClass::class, 'myStaticMethod']
|> new MyClass()->myInstanceMethod(...)
|> my_function(...);
echo $result;
function my_function(string $x): string {
return substr($x, 0, 10);
}
class MyClass {
public function __invoke(string $x): string {
return str_rot13($x);
}
public function myInstanceMethod(string $x): string {
return hash('sha256', $x);
}
public static function myStaticmethod(string $x): string {
return str_replace('E', 'O', $x);
}
}
此外,它也可以是返回 callable 的表达式:
$result = "Hello World"
|> 'strtoupper'
|> get_callable();
echo $result; // 787ec76dcafd20c1908eb0936a12f91edd105ab5cd7ecc2b1ae2032648345dff
function get_callable(): callable {
return fn($x) => hash('sha256', $x);
}
只接受接收第一个参数的 callable
管道操作符的一个主要限制是,所有链条中的 callable
必须只有一个必需的参数。
对于内置函数,如果该函数不接受任何参数,它不能用在链式调用中。对于用户空间 PHP 函数,将一个参数传入到不接受任何参数的函数中,不会导致任何错误,只是被静默忽略。
使用管道操作符,前一个表达式或者 callable 的返回值总是作为下一个 callable 的第一个参数传递。你无法修改参数的位置。
类型强制转换
当传递值到下一个 callable 时,管道操作符不会修改 PHP 的强制类型转换的实现方式。
当 strict_mode
启用时,它要求类型严格匹配。
declare(strict_types = 1);
$result = 1 |> strlen(...);
TypeError: strlen(): Argument #1 ($string) must be of type string, int given in ...
返回类型为 void
的 callable
返回类型为 void
的 PHP 函数/callable 也可以在链中调用,不过它们的返回值将被强制转换为 null
。void
函数通常用在链式调用的最后一个 callable
而不是用在中间,因为此后的链路只会得到 null
。
使用引用参数的函数
由于管道的中间值不存储在通常的变量中,因此使用期望参数通过引用传递的函数将不被允许。
explode("-", 'a-b-c') |> array_pop(...);
Error: array_pop(): Argument #1 ($array) could not be passed by reference in
不过,这个规则有一个例外:即内置的 PHP 函数可以使用 @prefer-ref
声名函数。传递直接值到这些函数不会导致错误。
截至 PHP 8.5,PHP 内核中只有两个这样的函数。FFI 扩展的 FFI
类也有一些方法支持该声名,不过它们不大可能使用管道操作符调用。
explode("-", 'a-b-c') |> array_multisort(...);
// true
['foo' => 'hello', 'bar' => 2] |> extract(...);
echo $foo; // hello
上述代码使用了 PHP 内核中唯二使用 @prefer-ref 引用的两个函数:array_multisort
和 extract
。
操作符优先级
callable 总是从左到右依次调用,除非使用 ()
对优先级进行了更改。
echo 10 + 6 |> dechex(...) |> hexdec(...);
// 16
上述代码中,10 + 6
最先进行了计算,然后将值传入后面的链路中。
类似于 PHP 中的其他操作符,使用 ()
允许改变优先级顺序:
echo 10 + (6 |> dechex(...)) |> hexdec(...);
// 22
当使用 null 断值或者三元操作符时,其优先级也是从左到右:
echo 10 ?? 6 |> dechex(...) |> hexdec(...);
// 10
echo null ?? 6 |> dechex(...) |> hexdec(...);
// 6
echo true ? 5 : 6 |> dechex(...) |> hexdec(...);
// 5
当管道操作符用做三元表达式或者 null 断值的左边表达式时管道优先执行:
echo 10 + 6 |> dechex(...) |> hexdec(...) === 16
? 'is sixteen'
: 'not sixteen';
// is sixteen
等号操作符 (==
和 ===
) 的优先级较低,因此像通常期待那样工作:
10 + 6 |> dechex(...) |> hexdec(...) === 16
// true
16 = 10 + 6 |> dechex(...) |> hexdec(...)
// true
向后兼容性影响
管道操作符是一个新的语法,因此无法对 PHP 8.4 及其之前的版本提供向后兼容性。
老版本中尝试运行使用管道操作符的 PHP 代码,将会导致解析错误:
Parse error: syntax error, unexpected token ">" in ...