探索 C# 10 的拓展 Lambda
C# 10 開始擴展 Lambda 表達式。C# 3 誕生 Lambda 表達式,基本夠用。不過極少數(shù)時候不得不需要一些特殊情況的時候,Lambda 表達式就不夠用了。比如
給 Lambda 表達式的參數(shù)和返回值添加特性;
給 Lambda 指定返回值類型,以便直接調(diào)用 Lambda。
為 Lambda 指定參數(shù)和返回值類型
為了明確 Lambda 的參數(shù)和返回值類型,我們發(fā)明了一種新的語法。原本的語法規(guī)則只能指定參數(shù)類型,但返回值類型無法固定。返回值類型直接使用和普通方法的語法一樣的、先類型后參數(shù)表列的方式表示:
比如這樣。返回值是 string
類型。
至于參數(shù)類型的聲明格式,這個是 Lambda 本來的語法,我就不多說了。因為 Lambda 沒有方法名,是即時聲明即時使用的一種語法,所以如果你記不住寫法,可以嘗試在參數(shù)表列前面補充一個方法名,還原成一個完整的方法聲明簽名的模式,來看是不是語法寫對了。
為 Lambda 指定特性
Lambda 在升級寫法之后,還可以為參數(shù)、返回值或 Lambda 方法本身標(biāo)記特性了。下面我們來用法和代碼。
從這個例子我們看到,我們給 () => { }
這個 Lambda 追加了特性 AAttribute
,這樣的話,Lambda 則自動對應(yīng)到不返回、無參數(shù)的方法簽名。
請注意,我們直接添加的 [A]
特性語法,這表示將整個 Lambda 表達式底層生成的方法標(biāo)記 A
特性。如果想對參數(shù)標(biāo)記特性的話,需要一對小括號:
我們對 a
這個參數(shù)標(biāo)記 [A]
特性,為了避免和 Lambda 表達式的標(biāo)記方式?jīng)_突,需要一對小括號。
如果標(biāo)記返回值上的話:
一些分析器語法分析的小問題
請注意如下語法:
?[
這樣的運算符,注意 b ? [A] () => { }
自然簽名
我們約定,把 Lambda 表達式聲明可以推出來的簽名稱為 Lambda 表達式的自然簽名??蓡栴}就在于,如果我們無法知道 Lambda 的參數(shù),就不可能得到它的自然類型。因此,我們給大家五個例子,讓大家明白自然簽名的推斷方式。
應(yīng)該從例子看得出來,前面三個因為無法暗示出參數(shù)類型和返回值類型,我們無法得到明確的自然簽名;但是后面兩個可以。比如 f4
變量的 Lambda:() => 1
,因為返回值是 int
類型的字面量,而參數(shù)是空的,所以它自動對應(yīng)無參數(shù)返回 int
類型的 Func<int>
類型;最后一個因為帶有返回值類型 string
,因此自動對應(yīng) Func<string>
。
直接調(diào)用 Lambda
Lambda 表達式以前不支持直接調(diào)用,現(xiàn)在可以用 var
關(guān)鍵字表示其類型了,因此……還是不行 :(
這個主要是因為會增加代碼復(fù)雜度,降低可讀性,以及分析器分析的復(fù)雜度等等,所以 C# 10 里仍然沒有實現(xiàn)這個功能。不過現(xiàn)在有了 var
來表達類型,就不必那么復(fù)雜了:
之類的。
Lambda 對 Delegate
和 object
類型的隱式轉(zhuǎn)換規(guī)則
因為之前說過,Lambda 最終是賦值給一個 Action
或者 Func
這樣的委托類型的,而委托類型默認從 Delegate
類型派生,因此所有的委托對象都可以賦值給 Delegate
類型(多態(tài))。
可是問題在于,我們無法確定它的類型,就無法直接賦值,比如前面的 () => default
語法。下面我們來幾個例子。
第一個例子 1.GetHashCode
和第三個例子 (int x) => x
因為確定了返回值和參數(shù)類型,因此包含自然簽名,可以轉(zhuǎn)換;但是如果使用 x => x
這樣的語法,因為沒有自然簽名,就無法轉(zhuǎn)換。
然后是第二個例子和第一個例子的書寫??赡苣憧粗鴦e扭。這是什么意思呢?因為 GetHashCode
和 ToString
此時是實例方法,需要默認代入一個實例才可參與計算,所以在使用的時候,我們需要優(yōu)先指定實例是什么。比如這里的 1.GetHashCode
。1
是調(diào)用 GetHashCode
的實例,而 GetHashCode
自身又可以明確確定簽名,所以賦值是可以成功的;不過,2.ToString
就不行了。因為 ToString
調(diào)用的實例是 int
類型(字面量 2
是 int
類型的),但 int
類型的 ToString
方法包含重載,所以無法確定到底是哪個調(diào)用,因此也會出錯。
特別說明一下 GetHashCode
和 ToString
這樣連參數(shù)表列都不帶的書寫方式。這一點 Lambda 里在 C# 5 開始就有。如果調(diào)用的方法(顯式方法名,比如 ToString
這樣帶名字的方法,而不是 Lambda 這種沒名字的方法)的簽名和調(diào)用時候需要的變量(參數(shù))本身給定的簽名一致,就允許直接寫名字:
比如這樣。
Lambda 對重載方法的最優(yōu)選取
有些時候,Lambda 現(xiàn)在可以暗示簽名之后就會出現(xiàn)更多的轉(zhuǎn)換的問題。
比如這樣。Invoke
方法直接有三個重載,分別是 Func<string>
、Delegate
和 Expression
類型接收 Lambda 作為參數(shù)??蓡栴}就在于,如果我們直接傳入方法名稱后,就無法確定執(zhí)行的方法的類型了。
C# 3 出現(xiàn) LINQ 的時候,就已經(jīng)開始允許 Lambda 表達式直接賦值給 Expression
類型。Expression
類型表示的是這個表達式自身的語法樹結(jié)構(gòu)。也就是說,C# 3 允許 Lambda 賦值給 Expression
類型,是為了直接把 Lambda 轉(zhuǎn)換成語法樹,方便 LINQ 里的語義轉(zhuǎn)換。
那么,既然三種類型都可接收 Lambda 的話,現(xiàn)在調(diào)用的方法究竟又是如何選擇的呢?很簡單,找匹配即可。為了規(guī)避 C# 3 語義上給定的語法規(guī)則沖突,如果自然簽名類型契合的話,自然是選擇最優(yōu)的;但是如果不契合的話,如果是方法名,就選取 Delegate
;如果是 Lambda,就選取 Expression
,比如上面這樣的例子。因為 C# 3 的 Expression
賦值 Lambda 是一個已經(jīng)存在的語法,所以不能改變;而 GetInt
方法既可以賦值給 Delegate
類型又可以賦值給 Expression
類型的話,會選擇 Delegate
一邊,因為這也是更合適合理的傳遞參數(shù)的規(guī)則。