PHP 7.4 新特性 —— 箭头函数 2.0

介绍

即使 PHP 中的匿名函数只执行简单的操作,但编写起来仍可能非常冗长。部分原因是由于需要手动导入已声明的变量,导致需要使用大量的语法样板。这使得使用简单闭包的代码难以阅读和理解。此 RFC 为这种模式提供了更简洁的语法。

作为讨论的开始,请观察我在网上找到的这个例子

function array_values_from_keys($arr, $keys) 
{
    return array_map(function ($x) use ($arr) {  
        return $arr[$x] ;  
    }, $keys); 
}

闭包执行过程中有些实现是多余的,在语法样板中会被弃用。箭头函数会将函数减少到以下内容: $arr[$x]

function array_values_from_keys($arr, $keys) 
{
    return (fn array_map($x) => $arr[$x], $keys);
}

过去已经广泛讨论了短闭包的问题。之前的 短闭包 RFC 经过投票被拒绝。该提案试图选择通过不同于先前被否决提案的语法来解决一些引起反感的问题。

此外,此 RFC 还对于包括不同语法备选方案以及绑定语义的冗长问题进行了讨论。不幸的是,由于受语法和实现的严重限制,短闭包是我们不太可能找到「完美」解决方案的主题。我们认为这个提议做出了「最不好」的选择。短闭包已经过时了,在某些时候我们必须在这里做出妥协,而不是搁置这个话题再过几年。

提案

箭头函数具有以下基本形式:

fn (parameter_list) => expr

当在父作用域中定义表达式中使用的变量时,它将通过值隐式捕获。在以下示例中,函数 $fn1$fn2 行为相同:

$y = 1 ;

$fn1 = fn ($x) => $x + $y;

$fn2 = function ($x) use ($y) { 
    return $x + $y; 
};

如果箭头函数嵌套,也有效:

$z = 1; 
$fn = fn ($x) => fn ($y) => $x * $y + $z;

这里外部函数捕获 $z。然后内部函数也从外部函数中捕获 $z 。总体效果是 $z 从外部范围变为内部函数可用。

功能签名

箭头函数语法允许任意函数签名,包括参数和返回类型、默认值、可变参数,以及按引用传递和返回。以下所有内容都是箭头函数的有效示例:

fn (array $x) =>  $x ; 
fn () : int => $x;  
fn ($x = 42) => $x;
fn (&$x) => $x;
fn &($x) => $x;
fn ($x), ...$rest) => $rest;

$this 绑定和静态箭头函数

就像普通的闭包一样,$this 当在类方法中创建一个短闭包时、变量,作用域和 LSB 作用域会自动绑定。对于正常的闭包,可以通过为它们添加 static 前缀来防止这种情况。为了完整起见,箭头功能也支持此功能:

class Test { 
    public function method() 
    { 
        $fn = fn () => var_dump($this); 
        $fn();  //对象(测试)#1 {...}

        $fn = static fn () => var_dump($this); 
        $fn();  //错误:在不在对象上下文中时使用 $ this 
    } 
}

静态闭包很少使用:它们主要用于防止 $this 循环,这使得 GC 行为不易预测。大多数代码都不需要关注它。

有人建议我们可以利用这个机会将 $this 绑定语义更改为仅在实际使用时才绑定 $this。除了 GC 效果,这将导致相同的行为。不幸的是,PHP 的 $this 有一些隐含的用法。例如,如果 Foo::bar() 调用 $thisFoo 范围兼容,则可以继承。我们只能对潜在 $this 用途进行保守分析,从用户的角度来看这是不可预测的。因此,我们倾向于 $this 始终保持绑定现有的行为。

按值变量绑定

如上所述,箭头函数使用按值变量绑定。这大致相当于为箭头函数对每个变量  $x 执行 use ($x) 。按值绑定意味着无法修改外部作用域中的任何值:

$x  =  1 ; 
$fn  = fn () =>  $x++;  //无效
$fn(); 

var_dump($x);  // int(1)

有关其他可能的绑定模式及其权衡的讨论,请参阅讨论部分。

隐式生成的使用与显式的使用之间存在细微差别:如果变量在绑定时未定义,则隐式使用不会生成未定义变量的提示。这意味着以下代码只生成一个提示(尝试使用 $undef 时),而不是两个(尝试绑定 $undef 时和尝试使用时):

$fn = fn () => $undef ; 
$fn();

这样做的原因是我们不能(由于引用)总是确定是读取还是写入变量或两者都是。考虑以下一些人为的例子:

$fn = fn ($str) =>  preg_match($regex, $str, $matches) && ($matches[1]%7 == 0)

这里 $matches 是填充的,并且在 preg_match() 被调用之前不需要存在。在这种情况下,我们不希望生成人为的未定义变量提示。

最后,自动绑定机制仅考虑字面上使用的变量。也就是说,下面的代码将生成一个未定义的变量提示,因为 $x 在函数内部没有使用语法,因此没有绑定:

$x = 42 ; 
$y = 'x' ; 
$fn = fn () => $$y;

当遇到变量改变时,可以通过使用更通用的绑定机制(绑定所有内容而不是绑定使用的内容)来添加对此的支持。它被排除在这里,因为它似乎是一个完全不必要的复杂实现,但如果人们认为有必要,它也可以得到支持。

优先权

箭头函数的优先级最低。这意味着 => 将尽可能消费右边的表达式:

fn ($x) => $x + $y 
//是 fn ($x) => ($x + $y)
//不是
(fn ($x) => $x) + $y

向后不兼容的变化

不幸的是,fn 关键字必须是完整的关键字,而不仅仅是保留函数的名称。

Ilija Tovilo 分析了 GitHub 上的前1000 个 PHP 存储库,以找到它们 fn 的用法。 这里提供了更多的信息,但粗略地发现是目前所有已知的 fn 用法都在测试中,除了它是命名空间段的情况。(命名空间使用恰好在我自己的库中,并且我很乐意重命名它)

例子

这些示例是从先前版本的箭头函数 RFC 复制而来的。

取自 silexphp / Pimple


$extended  =  function  ($c) use ($callable, $factory) { 
    return  $callable ($factory ($c), $c); 
};

//带箭头功能:
$extended = fn ($c) => $callable ($factory($c), $c);

这样可以将样板量从 44 个字符减少到 8 个。

取自 Doctrine DBAL

$this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) { 
    return in_array($v, $names); 
});

//使用箭头函数
$this->existingSchemaPaths = array_filte($paths, fn ($v) => in_array($v, $names));

这样可以将样板量从 31 个字符减少到 8 个。

许多库中的补充函数:

function complement (callable $f) { 
    return function (... $ args) use ($f) { 
        return !$f (...$args); 
    }; 
}

//带箭头功能:
function complement (callable $f) { 
    return fn (...$args) => !$f(...$ args); 
}

本文涉及的基础函数

参考

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 2

每次看到箭头函数就难受...

4年前 评论
LeoYao (楼主) 4年前
LOST

PHP 这是要向 JavaScript 看齐嘛

4年前 评论
LeoYao (楼主) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!