PHP 7.4 新特性 —— 箭頭函式 2.0

LeoYao發表於2019-06-04

介紹

即使 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); 
}

本文涉及的基礎函式

參考

相關文章