[.NET]快快樂樂學LINQ系列前哨戰-Func, Action, Predicate

[.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 的。

image

原本使用 delegate 基本上需要四個步驟:

  1. 宣告 delegate 的 type
  2. 定義滿足 delegate type 簽章的方法內容
  3. 建立一個 delegate 的 instance
  4. 呼叫 delegate instance 的 Invoke 方法,並將對應的參數傳入。

也因為之前的 delegate 使用太過囉嗦,所以會有匿名委派,進而後面的 Lambda 。

 

再來看另一種情境,如下:

image

只因為型別不同,導致要寫相當多的多載方法,這時候可以透過泛型(generic)來簡化寫法,如下圖所示:

image

如果上面那兩個方法的使用場景是需要透過 delegate 來呼叫,那就得宣告好幾種因應不同 type 的 delegate ,那程式碼就更囉嗦了,如下圖所示:

image

這時候,只需要使用泛型委派就可以有泛型的簡潔,以及委派的彈性,如下圖所示:

image

當然啦,以上面的例子如果 n 與 d 需要同一個型別,那我們應該只需要 T1 即可,不需要 T2 。如果連 return 的 type 也要一樣的話,那泛型委派應該宣告成:
public delegate T AdditionDelegate<T>(T n, T d);

上面的這個範例,雖是泛型委派,但仍是具名的,也就是我們得先宣告一個 delegate 的 type ,才能開始用它。

如果把上面變成匿名委派呢?也就是把明確定義委派的功夫省略,這時候,我們就可以用 Func 。

先來看一下,對應上面的例子,如果換成 Func 時,會有什麼差異。如下圖所示:

image

在 context 端要使用原本的 AdditionDelegate 的部份,直接用 Func<T1, T2, TResult> 就可以取代了。

 

所以,究竟什麼是 Func<T1, T2, TResult> 呢?

image

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 常見的方法簽章:

image

除了第一個 this IEnumerable<TSource> 為擴充方法所針對的型別外,需額外傳入一個 Func<TSource, TResult> 的參數。(也就是要傳入一個委派,傳入一個型別為 TSource 的參數, 回傳一個型別為 TResult 的回傳值)

Context 端的說明如下圖所示:

image

以上圖的例子,這邊的 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);

image

舉個例子,List<T>.ForEach(),就是傳入 Action<T> 的參數。

image

還有個跟 Func 比較不一樣的地方,就是 Action 可以是一個非泛型的委派,因為 delegate 可以沒有參數,也可以是 void 。因此 Action() 就代表 public delegate void Action(); 搭配 Lambda 使用上就會像下面這樣:

image

 

Func<T> 與 Action<T> 簡單的比較圖,如下圖所示:

image

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() ,如下圖所示:

image

這邊的 p => p.Age >= 18 ,其實就是 Func<Person,bool> 的型別,只是跟上面的 Select() 例子一樣,使用 Lambda 來表示匿名方法的內容。

 

結論

簡單摘要一下:

  1. Func<T1, T2, TResult> 就是代表一個 delegate ,需傳入 2 個參數,其型別分別為 T1 與 T2 ,且這個 delegate 會回傳 TResult 型別的回傳值。
  2. Action<T1, T2> 就是代表一個 delegate ,需傳入 2 個參數,其型別分別為 T1 與 T2 ,且這個 delegate 是回傳 void 。
  3. 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/