[.NET]快快樂樂學LINQ系列前哨戰-內建的委派型別
前言
距離上一篇 [.NET]快快樂樂學LINQ系列前哨戰-Lambda的簡介 已經 15 個月了(汗)。
承續上一篇提到 delegate 從 C# 1.0 一路簡化到 C# 3.0 Lambda 的方式,接下來這一篇則是要介紹三個 LINQ 常用到內建的委派型別:Func<T1, TResult> 、 Action<T1> 跟 Predicate<T> 。
看完這篇文章的說明之後,以後看 MSDN 中那一堆看起來很恐怖的方法簽章,就不會再這麼害怕了。
Func<T1, TResult> 緣由
介紹 Func 之前,先來回顧一下,之前我們是怎麼使用 delegate 的。
原本使用 delegate 基本上需要四個步驟:
- 宣告 delegate 的 type
- 定義滿足 delegate type 簽章的方法內容
- 建立一個 delegate 的 instance
- 呼叫 delegate instance 的 Invoke 方法,並將對應的參數傳入。
也因為之前的 delegate 使用太過囉嗦,所以會有匿名委派,進而後面的 Lambda 。
再來看另一種情境,如下:
只因為型別不同,導致要寫相當多的多載方法,這時候可以透過泛型(generic)來簡化寫法,如下圖所示:
如果上面那兩個方法的使用場景是需要透過 delegate 來呼叫,那就得宣告好幾種因應不同 type 的 delegate ,那程式碼就更囉嗦了,如下圖所示:
這時候,只需要使用泛型委派就可以有泛型的簡潔,以及委派的彈性,如下圖所示:
當然啦,以上面的例子如果 n 與 d 需要同一個型別,那我們應該只需要 T1 即可,不需要 T2 。如果連 return 的 type 也要一樣的話,那泛型委派應該宣告成:
public delegate T AdditionDelegate<T>(T n, T d);
上面的這個範例,雖是泛型委派,但仍是具名的,也就是我們得先宣告一個 delegate 的 type ,才能開始用它。
如果把上面變成匿名委派呢?也就是把明確定義委派的功夫省略,這時候,我們就可以用 Func 。
先來看一下,對應上面的例子,如果換成 Func 時,會有什麼差異。如下圖所示:
在 context 端要使用原本的 AdditionDelegate 的部份,直接用 Func<T1, T2, TResult> 就可以取代了。
所以,究竟什麼是 Func<T1, T2, TResult> 呢?
Func<T1, T2, TResult> 其實就是 C# 幫忙將上面的 public delegate 宣告封裝起來而已(就像 EventHandler 也是這樣的語法糖衣)。
也就是 Func<T1, T2, TResult> 可以這麼解釋:有一個 delegate,需要傳入 2 個參數,其型別分別為 T1 與 T2 ,而回傳值型別為 TResult 。
也因此, Func 一定至少會有個泛型型別 TResult ,因為方法可以沒有參數,但一定要有回傳值。(誰說方法一定要有回傳值,我不能是 void 嗎?答案是可以的, void 的部份,是 Action<T> 在做的事)
這邊我們以 LINQ 中的 Select<TSource, TResult>() 方法來當個例子,先看到 Select 常見的方法簽章:
除了第一個 this IEnumerable<TSource> 為擴充方法所針對的型別外,需額外傳入一個 Func<TSource, TResult> 的參數。(也就是要傳入一個委派,傳入一個型別為 TSource 的參數, 回傳一個型別為 TResult 的回傳值)
Context 端的說明如下圖所示:
以上圖的例子,這邊的 p => p.Name
的型別,其實就是用 Lambda 來表示匿名方法的匿名委派,其型別為 Func<TSource, TResult> ,在這例子 generic 的部份, TSource 就是 typeof(Person) , TResult 則是 string 。
(至於 Select 方法的實作內容,後續我們會提到)
總結一句話,Func<T1, T2, …, Tn, TResult> 就代表一個 delegate ,有 n 個參數,其型別分別為 T1, T2, …, Tn,有一個回傳值,其型別為 TResult ,這也是為什麼這個型別參數會被命名為 TResult 的原因。
Action<T1>
有了前面 Func 的簡介,要介紹 Action 就簡單多了。
Func 就是把會回傳 TResult 的 delegate 封裝起來,而 Action 就是把回傳 void 的 delegate 封裝起來。
所以 Action<T1> 攤開就等同於:
public delegate void Action<T1>(T1 obj);
舉個例子,List<T>.ForEach(),就是傳入 Action<T> 的參數。
還有個跟 Func 比較不一樣的地方,就是 Action 可以是一個非泛型的委派,因為 delegate 可以沒有參數,也可以是 void 。因此 Action() 就代表 public delegate void Action(); 搭配 Lambda 使用上就會像下面這樣:
Func<T> 與 Action<T> 簡單的比較圖,如下圖所示:
Func<T> 的 T 是回傳型別,而 Action<T> 的 T 是第一個參數的型別。
Predicate<T>
介紹完最基底的兩種 delegate: Func 與 Action 後,接下來要介紹 Predicate<T> ,這個就跟 EvenHandler<T> 的情境比較接近一些。 EventHandler 其實就是 public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) 的封裝。因為事件幾乎都是以這樣的簽章存在。
而 Predicate<T> 呢? 其實就是下面這個 delegate 的封裝:
public delegate bool Predicate<T>(T obj);
一言以蔽之,Predicate 就是:一個委派,需傳入一個參數型別為 T ,回傳值型別為 bool 。
Predicate<T> 常用在哪邊呢? 最常用在判斷式,例如 LINQ 的 Where() ,如下圖所示:
這邊的 p => p.Age >= 18
,其實就是 Func<Person,bool> 的型別,只是跟上面的 Select() 例子一樣,使用 Lambda 來表示匿名方法的內容。
結論
簡單摘要一下:
- Func<T1, T2, TResult> 就是代表一個 delegate ,需傳入 2 個參數,其型別分別為 T1 與 T2 ,且這個 delegate 會回傳 TResult 型別的回傳值。
- Action<T1, T2> 就是代表一個 delegate ,需傳入 2 個參數,其型別分別為 T1 與 T2 ,且這個 delegate 是回傳 void 。
- Predicate<T> 就代表一個 delegate ,需傳入 1 個參數,其型別為 T ,且這個 delegate 是回傳 bool。
而在 LINQ 中,Enumerable 這個 static class 幾乎都是針對 IEnumerable<TSource> 設計擴充方法,而後面往往要傳入的 delegate ,基本上就是將 IEnumerable<TSource> 展開 (用GetEnumerator() 或 foreach),然後每一個 TSource 再傳入 delegate 當參數,取得回傳值,處理之後 yield return 就會變成 IEnumerable<TSource> 或 IEnumerable<TResult> ,回傳單一值的話,就不會有 yield return。
感謝
感謝同事 Titan 與小林一起設計 training 的教材!
blog 與課程更新內容,請前往新站位置:http://tdd.best/