箭头函数是一种简短的匿名函数:fn($x) => $x * 2。=> 后面的函数体是一个表达式,它的值就是返回值;表达式用到的外部变量会被自动按值捕获。箭头函数从 PHP 7.4 开始提供,最常用来写回调。
<?php
$double = fn($x) => $x * 2;
echo $double(21), "\n";
$prices = [10, 25, 40];
print_r(array_map(fn($p) => $p * 1.2, $prices));
$rate = 0.9; // used inside without "use"
print_r(array_map(fn($p) => $p * $rate, $prices));
语法
fn、参数列表、=>,再加一个表达式。参数类型、默认值和返回类型的写法和普通函数完全一样:
<?php
$add = fn(int $a, int $b = 1): int => $a + $b;
$greet = fn(string $name): string => "Hello, $name";
$isEven = fn(int $n): bool => $n % 2 === 0;
$now = fn() => 'no parameters';
echo $add(2, 3), ' ', $add(2), "\n";
echo $greet('Ada'), "\n";
var_dump($isEven(4), $isEven(7));
echo $now(), "\n";
// Call it immediately
echo (fn($x) => $x ** 2)(9), "\n";
箭头函数没有 return 关键字,也没有花括号。为了好读,很长的表达式仍然可以拆成几行。
外部变量按值捕获
箭头函数能看到创建它时所在作用域里的每个变量,并在那一刻复制它们的值。在函数里做的修改只留在函数里,函数创建之后在外面做的修改它也看不到:
<?php
$count = 0;
$increment = fn() => ++$count;
echo $increment(), "\n"; // works on its own copy
echo $increment(), "\n"; // a fresh copy each call
echo $count, "\n"; // the outer variable is unchanged
$discount = 10;
$apply = fn($price) => $price - $discount;
$discount = 50; // too late: 10 was already captured
echo $apply(100), "\n";
这就是它和 function () use (&$count) 的主要区别:后者可以按引用共享一个变量,箭头函数则不能给外部变量重新赋值。不过它仍然可以修改捕获到的对象(副本是指向同一个对象的句柄),而按引用声明的参数也和普通函数一样有效:fn(array &$list) => $list[] = 'x'。
作为 array_map、array_filter 和 usort 的回调
大多数数组函数都接收回调,箭头函数在这里最能减少冗余代码。对比一下同一个过滤操作的两种写法:
<?php
$users = [
['name' => 'Ada', 'age' => 36, 'active' => true],
['name' => 'Linus', 'age' => 28, 'active' => false],
['name' => 'Grace', 'age' => 45, 'active' => true],
];
// Long form
$active1 = array_filter($users, function ($u) {
return $u['active'];
});
// Arrow function
$active2 = array_filter($users, fn($u) => $u['active']);
var_dump($active1 === $active2);
$names = array_map(fn($u) => $u['name'], $active2);
echo implode(', ', $names), "\n";
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
echo implode(', ', array_column($users, 'name')), "\n";
$total = array_reduce($users, fn($sum, $u) => $sum + $u['age'], 0);
echo "Total age: $total\n";
<=> 返回 -1、0 或 1,正好是 usort() 需要的值,所以按某个字段排序只要一行。把 $a 和 $b 对调就能降序排序。
返回箭头函数的箭头函数
被捕获的变量也包括外层箭头函数的参数,所以你可以把箭头函数嵌套起来,用一些值生成新的函数:
<?php
$multiplyBy = fn($factor) => fn($x) => $x * $factor;
$triple = $multiplyBy(3);
$tenTimes = $multiplyBy(10);
echo $triple(5), ' ', $tenTimes(5), "\n";
$between = fn($min, $max) => fn($n) => $n >= $min && $n <= $max;
$teen = $between(13, 19);
print_r(array_filter([8, 13, 16, 21, 19], $teen));
注意 array_filter() 会保留原来的键,所以结果从索引 1 开始。
fn 与 function:什么时候需要完整的闭包
当函数体需要不止一个表达式,或者必须修改外部变量时,箭头函数就不够用了。这时请使用匿名函数:
| fn($x) => ... | function ($x) use ($y) { ... } |
|---|
| 函数体 | 一个表达式 | 任意语句 |
| 返回 | 自动返回 | 显式 return |
| 外部变量 | 用到的全部自动捕获 | 只有 use 中列出的 |
| 按引用捕获 | 不支持 | 支持,use (&$y) |
| 起始版本 | PHP 7.4 | PHP 5.3 |
<?php
$orders = [120, 45, 300, 80];
// Needs a statement and an outer total: a closure with use (&...)
$total = 0;
$big = [];
array_walk($orders, function ($amount) use (&$total, &$big) {
$total += $amount;
if ($amount > 100) {
$big[] = $amount;
}
});
echo "Total $total, big orders: ", implode(', ', $big), "\n";
// The same result split into expressions: arrow functions fit again
$total2 = array_sum($orders);
$big2 = array_values(array_filter($orders, fn($a) => $a > 100));
echo "Total $total2, big orders: ", implode(', ', $big2), "\n";
如果一个箭头函数写起来很憋屈,往往说明这件事其实是两件事。像上面那样把它拆成一次过滤和一次求和,每个回调就又能保持为一个表达式。