第 73 講:C# 2 之匿名函數(shù)(一):委托實例化的簡化
還記得前面學習的委托嗎?我們?nèi)允褂门判騺砼e例。
Part 1 引例
假設(shè),我們需要使用委托傳參的方式來表示數(shù)組排序 Array.Sort
方法,而我們也學習了如何調(diào)用這個方法:
是的,這樣的代碼就可以完成排序??墒?,這樣代碼仍然很長。因為我們要自己寫一個方法,而且還得使用 new Comparison
來實例化委托對象來作為傳參過程。因此,C# 2 開始,為了簡化語法,發(fā)明了一種新的語法格式:匿名函數(shù)(Anonymous Function)。
Part 2 匿名函數(shù)的語法
我們先來看簡化版代碼應該怎么寫:
當然,也可以換個行:
是的,就兩個語句即可。其中第二個語句有些奇怪。第一個參數(shù)仍然沒有改變,但第二個參數(shù)變長了。其實也是猜得到的:因為實際的方法不見了,改成了這樣的語法被替換到了參數(shù)的位置上。
仔細看這個參數(shù)寫法 delegate (int a, int b) { return a.CompareTo(b); }
,它就被稱為匿名函數(shù)。
匿名函數(shù)的語法如下:
匿名函數(shù)的作用就是為了簡化委托類型的實例化。它的寫法是直接將“方法的關(guān)鍵部分”參數(shù)化,直接寫進參數(shù)里。這種寫法大大改變了你書寫代碼(特別是用委托的時候)的風格和習慣。因此,你必須要習慣這種書寫格式。
它的語法格式是這樣的。參數(shù)表列直接對應了我們這里的 Comparison<T>
委托類型的參數(shù)表列。因為我們這里對數(shù)組排序,因此比較的是兩個 int
數(shù)據(jù),因此我們直接寫 (int a, int b)
即可;與此同時,我們直接把執(zhí)行代碼和前面的 a.CompareTo(b)
抄過來就可以了。因此,這個匿名函數(shù)長這樣。
之所以叫匿名函數(shù),就是因為它沒有方法名稱,而不論什么時候,委托的實例化代換為匿名函數(shù)語法的時候都是直接退化為了 delegate
關(guān)鍵字了,所以這樣的方法是沒有名字的。
當然,也有其它的方法也可以使用到這個語法。比如這樣的代碼:
這個 Array.TrueForAll
方法表示驗證整個數(shù)組序列是不是全部的元素都滿足同樣的條件。而這個條件,就從第二個參數(shù)進行指定。所以,第二個參數(shù)是一個條件的委托實例。我們這里只需要驗證元素的數(shù)據(jù),所以這個匿名函數(shù)需要一個參數(shù),并返回 bool
結(jié)果。
是的,你說對了。Array.TrueForAll
方法的第二個參數(shù)的類型就是 Predicate<T>
。
Part 3 委托實例化轉(zhuǎn)匿名函數(shù)的套路
其實前面基本上可以搞懂如何簡化這樣的代碼,用上匿名函數(shù)。不過,我還是簡單說一下,如何從原生代碼轉(zhuǎn)換為匿名函數(shù)語法。
3-1 第一步:看委托類型的簽名,確定參數(shù)和返回值類型
如何轉(zhuǎn)換語法?先看委托類型。匿名函數(shù)用就用在委托類型實例化之上,所以一定要把眼睛關(guān)注到委托類型上。先看委托類型的簽名,這樣就可以確定返回值和參數(shù)的類型。
比如說,我的委托類型定義為這樣:
那么它的參數(shù)類型是 ref int
、ref int
,而返回值類型則是 void
。
3-2 第二步:腦內(nèi)推測方法的完整語法
第二步是在腦內(nèi)構(gòu)造一個方法,契合這個簽名的。按照委托類型的尿性,我們一般不在意方法是不是 static
修飾的,所以我們不必去管方法自身的修飾符,只大概想一想它的參數(shù)和返回值放上去后是啥樣的。
這個時候,得看方法的實際使用過程來確定具體的代碼執(zhí)行邏輯。因為 Swapper
委托類型的名字是“交換”的類似意思,所以我們實現(xiàn)的時候就得考慮是不是用于交換變量了。顯然,參數(shù)類型都是帶有 ref
修飾符的,它的意圖已經(jīng)很明顯了:沒有 ref
修飾符就是復制副本,因此無法完成交換過程。正是因為交換需要方法內(nèi)外都使用同一個數(shù)據(jù)信息,因此 ref
修飾符才得以存在。正是因為這個修飾符,所以我們才可以推斷得到這樣的實現(xiàn)邏輯。
那么既然是交換,我們就直接在里面寫上交換邏輯即可。
3-3 第三步:去掉返回值類型,把方法名替換為 delegate
關(guān)鍵字
那么,匿名函數(shù)的作用是把 匿名函數(shù)語法
代替掉原來的 new 委托(方法名)
,因此我們僅需要替換掉返回值類型和方法名。于是乎,剛才的代碼可以改寫成這樣:
至此,匿名函數(shù)就改寫完成了。
3-4 特殊情況:無參匿名函數(shù)
這里稍微說一下一種特殊情況:無參數(shù)的匿名函數(shù)。
C# 允許無參匿名函數(shù)省略這參數(shù)的這對小括號。所謂的省略,就是允許直接不寫這對括號。假設(shè)一個委托類型是 Action
的話,那么這種情況的方法的簽名里,參數(shù)是為空的。正是因為這樣的情況,匿名函數(shù)的原本語法應該長這樣:
而這種情況下,小括號可以不寫。因此,無參匿名函數(shù)的寫法可以去掉這對小括號:
Part 4 匿名函數(shù)的底層原理
既然都說到這里了,那么還是有必要說明一下,這個 C# 的匿名函數(shù),它的完整版是怎么樣的。
4-1 Show you the code
廢話不多說直接上代碼。不過我們還是得舉例說明,才能說明底層原理。假設(shè),我們有這么一個委托類型的實例,賦的是一個匿名函數(shù)。請仔細觀察此代碼:
delegate (int v) { return v % 2 != 0; }
這,就是 C# 的編譯器對匿名函數(shù)的實現(xiàn)。換句話說,這段代碼才是匿名函數(shù)的“真相”。讓我們來分析這一段代碼。
4-2 Closure
類型本身
首先,我們是把 predicate
實例化的這么一個過程擴展為了一個類類型,就是這個所謂的 Closure
類型。這個設(shè)計其實挺奇怪的,對吧。我們最初的辦法是在 Main
方法的所在類型里單獨創(chuàng)建一個方法,然后把方法名抄到 new 委托
的實例化過程里去。可現(xiàn)在編譯器居然生成了一個 Closure
類型,著實讓我們初學的時候摸不著頭腦。不過,要解釋這個問題,我們先不著急。下一講內(nèi)容我們會探討這個問題。
先來看看它自身的修飾符。private
、sealed
。有趣。private
是防止外部調(diào)用。因為這個類型是編譯器生成的代碼,那么它自然就有一個效果,即不允許任何別的地方亂用和濫用這種編譯器創(chuàng)建的類型,否則會導致執(zhí)行代碼的紊亂。所以,private
是有道理的。那么 sealed
呢?不讓創(chuàng)建派生類唄。沒有人會考慮去對一個編譯器生成的類型創(chuàng)建派生類的。但是,也不乏有人會這么去做,所以考慮到這種派生的隱藏的問題,編譯器生成的代碼自帶了 sealed
修飾符不讓任何人去從它派生別的類型出來。
而就類型本身來說,它還標記了兩個特性,分別是:
SerializableAttribute
:象征對象是進行二進制序列化和反序列化的操作的;CompilerGeneratedAttribute
:這個特性表示這個成員(或者類型)是編譯器自主生成的,跟用戶寫的代碼有關(guān)。
其中,序列化和反序列化是編程里較為重要的兩個概念,這個其實早已說明過,這里再次出現(xiàn),因此我們稍微回顧一下。
序列化指的是把一個編程里的實現(xiàn)的對象,它包含的數(shù)據(jù)信息存儲到電腦本地里(用一個二進制的文件來存儲)的這么一個過程;反序列化就是反過來:把一個本來就是原本序列化得到的這個二進制結(jié)果文件,給解析出來,得到對象本身的過程。
不過這個內(nèi)容已經(jīng)超出我們本教程的講解范疇,而且序列化在 .NET 6 框架提供的 API 里有更為方便和好用的新序列化模型:JSON 序列化,因此二進制序列化將會被拋棄掉,因此教程也不打算對二進制序列化作講解。
那么,看了一眼解釋和文字,我們需要了解的就只有這個 CompilerGeneratedAttribute
了。可是這個特性其實也沒啥多說的,它是代碼在生成后自動添加上去的特性,因此跟我們自己寫的代碼自身也沒有任何關(guān)系。畢竟,它是編譯器生成的代碼,那打個標記好像也沒啥不妥,對吧,反正特性也不影響程序執(zhí)行,只是在反射里會稍微用到一下。而且這個特性會在以后的新語法的底層經(jīng)常看到。
那么特性就不多說了。接下來我們來說說里面的實現(xiàn)過程和邏輯。
4-3 Closure
的兩個字段
那么,既然類型已經(jīng)創(chuàng)建好了,自然就得說明一下這里都有一些什么了。這個 Closure
類型雖然是一個類類型,但本身也不復雜,它只包含兩個字段和一個方法,沒別的了。
這兩個字段都是 static
修飾的,其中第一個是 readonly
的,即無法修改的 Instance
字段,就是它自己這個類型的字段,另外一個則是 CachedField
字段,是我們這里用到的 Predicate<>
委托類型的字段。
先來說 Instance
。Instance
是用于調(diào)取里面的方法實例之類的信息用的。而為什么要 readonly
呢?因為 new 類型()
唄。這實例化的是它自己這個類型的情況,那么肯定不會讓別人去隨意修改它,否則程序也會紊亂。那么為什么是 static
的呢?因為速度和效率。有沒有 static
關(guān)鍵字的區(qū)別在于,什么時候創(chuàng)建對象。static readonly
的字段的賦值過程會在程序初始化(還沒開始運行之前,會有一段時間去初始化程序的數(shù)據(jù)信息)的時候賦值。這樣在運行的時候就不會影響程序的運行速度和效率了;而沒有 static
的對象,是在運行時才會創(chuàng)建分配內(nèi)存空間和賦值,因此會慢一些??紤]到效率的問題,所以用到的是 static
。
而另外一個字段 CachedField
就是我們這里用到的目標字段了。它用于和記錄緩存的委托實例的結(jié)果??梢钥吹剿?static
修飾的,所以它不會受到對象實例化的影響而去單獨分配內(nèi)存之類的。但是它最開始是沒有存儲任何數(shù)值的,并且一直保持 null
這個數(shù)值結(jié)果。都說了是緩存嘛,緩存緩存那自然是運行的時候用到了才會去看它,對吧。
4-4 Method
方法
仔細看看它的實現(xiàn)代碼就可以發(fā)現(xiàn),欸?這不是我前面給的匿名函數(shù)里的執(zhí)行代碼嗎?是的,它被原封不動地抄寫了過來。而方法是實例的方法,且是 internal
修飾符修飾的。奇了怪了,為啥是 internal
而不是 private
?這就得說說,它在啥時候被用到了。
可以看到,在 Main
里,我們的那一段匿名函數(shù)被改成了 if
語句。如果 Closure
類型的 CachedField
靜態(tài)字段是為 null
的,那么我們就需要為其實例化和賦值。可以看到,它是在 Main
里使用到了 Method
實例方法。而 Method
這個方法是被放在了 Closure
這個類里了,它并不是在 Main
所在的類 Program
里面。因此,如果我們給 Method
方法標記的是 private
修飾符的話,那么我們無論如何也無法得到這個 Method
了。因此,這里用到的是 internal
。那么,protected
或 protected internal
行不行呢?當然不行,因為它又不是派生和繼承鏈上的一環(huán)。而 protected internal
則是擴大了 internal
修飾符的可訪問級別,它允許在項目外部以“從這個類型派生”的方式來得到它里面的內(nèi)容。哪怕我允許了 Closure
類型不標記 sealed
修飾符,開放繼承,也會造成隱藏的不安全性。因此,這里用的是 internal
,這也是目前最為合適的級別了。
至此,我們就相當于說明了,這個 Closure
類型的具體的內(nèi)容,以及匿名函數(shù)為什么要被編譯器改成這樣。當然,至于為什么要用類來封裝包裹一層方法,請聽下回分解。