之前的<C# 筆記:重訪委派--從 C# 1.0 到 2.0 到 3.0>已經交代過 lambda expressions 的語法及其來龍去脈,這篇筆記會先複習一下 lambda 表示式,然後進入 LINQ。
先用一個範例程式把前面幾篇 C# 筆記介紹的語法做個總複習吧!程式碼如下:
程式列表 1: LambdaExpressionDemo
此範例是個 console 程式,它用到了前幾篇文章提到的隱含型別、自動實作屬性與 Object Initializers、委派以及 lambda expression 等語法。如果你覺得這個範例程式裡面有些語法還很陌生,不妨回頭看一下之前的文章。
在寫<重訪委派>時,我曾在範例程式中宣告一個 Predicate 委派型別,其實就是仿照 .NET Framework 的 System.Predicate<T> 型別。當時為了簡單起見,我把泛型的部分拿掉了,這次則是直接用 Predicate<T>;它是個泛型委派(generic delegate)型別。
兩相比較,使用 lambda 表示式的寫法確實簡潔許多。
「程式列表 1」的第 26 行可以這麼解讀:建立一個 Predicate<Employee> 型別的委派物件,這個委派物件所要執行的委派方法需要傳入一個參數 e(編譯器會自動推測參數型別為 Employee),並傳回一個布林值,代表傳入的那個 Employee 物件是否就是我們要找的員工。
委派物件(和委派方法)準備好之後,接著第 27 行就把它丟給 List<T> 的 Find 方法,此方法會逐一走訪串列中的每個元素,而且每取出一個元素(型別是 Employee)就會執行一次我們指定的委派方法,以得知該元素是否符合搜尋條件。
綜上所述,我們可以整理一下,當你要把 C# 2.0 的委派寫法轉換成 lambda 表示式的寫法,主要有兩個動作:
但由於這個匿名方法的實作程式碼只有一行,所以我們可以再簡化一點,把大括弧和 return 都去掉,程式碼排成一行。此外,由於參數列只有一個參數,而且編譯器能夠從你宣告的委派型別 Predicate<Employee> 自動推測該參數的型別為 Employee,所以 (Employee e) 就可以把小括弧和型別名稱都去掉,變成只剩下參數名稱:e。這樣一路簡化下來,結果就是「程式列表 1」的第 26 行了。
以下列出從匿名方法到最簡的 lambda expression 寫法,你不妨開啟 Visual Studio 2008,親自實驗一下各種寫法,應該會更有感覺。
關於 lambda expression 的參數列,有些撰寫規則值得提一下:
它們是擴充方法(extension methods),來自 System.Linq.Enumerable 類別。它們各自有兩個版本,這裡各列出一個原型宣告:
從原型宣告可以看出,它們要擴充的是 IEnumerable 介面。此外,這三個方法都需要傳入一個
Func<T, R> 型別的參數,這個 Func<T, R> 是定義於 System 命名空間的泛型委派型別,其中參數 T 代表傳入參數的型別,參數 R 則為傳回值的型別。 Func<...> 泛型委派有五個版本,最多可有四個 T,也就是最多可傳入四個參數。這表示甚麼?這表示 .NET 為我們預先定義了五種泛型委派,當你的委派方法需要一個傳回值,以及有零至四個參數時,就不一定要定義新的委派型別,你也可以直接使用現成的 Func<...>。
了解 Func<...> 型別的作用之後,接著試試修改「程式列表 1」的範例,先改用 Where 找出姓名包含字母 'i' 的員工,並將它們印出來。結果如「程式列表 2」所示。
程式列表 2:使用 Where 擴充方法尋找員工
接著加上 OrderBy,把找到的員工清單以年齡排序。由於 Where 傳回的是 IEnumerable<T>,所以我們可以用串接的方式,直接把 OrderBy 呼叫接在後面。結果原本的第 10 行變成這樣:
IEnumerable<string>。方便起見,這裡宣告成 var 隱含型別。
呼~到這裡應該差不多了。我們已經沾到 LINQ 的一點邊,再往下寫就嫌長了。之所以提到 Func<...> 委派型別以及 Where、OrderBy、Select 等擴充方法,主要也是為了學習 LINQ 做暖身。最後就把剛剛的 Where+OrderBy+Select 的版本改用 LINQ 表示式來寫,做個對照:
相關文章
先用一個範例程式把前面幾篇 C# 筆記介紹的語法做個總複習吧!程式碼如下:
程式列表 1: LambdaExpressionDemo
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5:
6: namespace LambdaExpressionDemo
7: {
8: class Employee
9: {
10: // 自動實作屬性.
11: public string Name { get; set; }
12: public int Age { get; set; }
13: }
14:
15: class Demo
16: {
17: public void Run()
18: {
19: var empList = new List<Employee>() // 隱含型別
20: { // Object initializers(串列和 Employee 物件都有).
21: new Employee { Name = "Michael", Age = 20 },
22: new Employee { Name = "Douglas", Age = 25 },
23: new Employee { Name = "Jenifer", Age = 14 }
24: };
25:
26: Predicate<Employee> p = e => e.Name.StartsWith("J"); // Lambda
27: Employee emp = empList.Find(p);
28: Console.WriteLine(emp.Name);
29: }
30: }
31:
32: class Program
33: {
34: static void Main(string[] args)
35: {
36: Demo demo = new Demo();
37: demo.Run();
38: }
39: }
40: }
此範例是個 console 程式,它用到了前幾篇文章提到的隱含型別、自動實作屬性與 Object Initializers、委派以及 lambda expression 等語法。如果你覺得這個範例程式裡面有些語法還很陌生,不妨回頭看一下之前的文章。
在寫<重訪委派>時,我曾在範例程式中宣告一個 Predicate 委派型別,其實就是仿照 .NET Framework 的 System.Predicate<T> 型別。當時為了簡單起見,我把泛型的部分拿掉了,這次則是直接用 Predicate<T>;它是個泛型委派(generic delegate)型別。
Lambda Expressions
C# 3 的 lambda 表示式主要是將 C# 2.0 的匿名方法(anonymous methods)進一步簡化。以剛才「程式列表 1」的範例來說,如果不使用 lambda 表示式,那麼第 26 行就得這麼寫:Predicate<Employee> p = delegate(Employee e) { return e.Name.StartsWith("J"); };
兩相比較,使用 lambda 表示式的寫法確實簡潔許多。
「程式列表 1」的第 26 行可以這麼解讀:建立一個 Predicate<Employee> 型別的委派物件,這個委派物件所要執行的委派方法需要傳入一個參數 e(編譯器會自動推測參數型別為 Employee),並傳回一個布林值,代表傳入的那個 Employee 物件是否就是我們要找的員工。
委派物件(和委派方法)準備好之後,接著第 27 行就把它丟給 List<T> 的 Find 方法,此方法會逐一走訪串列中的每個元素,而且每取出一個元素(型別是 Employee)就會執行一次我們指定的委派方法,以得知該元素是否符合搜尋條件。
綜上所述,我們可以整理一下,當你要把 C# 2.0 的委派寫法轉換成 lambda 表示式的寫法,主要有兩個動作:
- 把關鍵字 delegate 去掉。
- 加上 => 運算子。這個運算子的左邊要放匿名方法的參數宣告,右邊則是匿名方法的本體。=> 的意思是 "goes to",亦即「丟到」、「傳入」的意思。
Predicate<Employee> p2 = (Employee e) => { return e.Name.StartsWith("J"); };
但由於這個匿名方法的實作程式碼只有一行,所以我們可以再簡化一點,把大括弧和 return 都去掉,程式碼排成一行。此外,由於參數列只有一個參數,而且編譯器能夠從你宣告的委派型別 Predicate<Employee> 自動推測該參數的型別為 Employee,所以 (Employee e) 就可以把小括弧和型別名稱都去掉,變成只剩下參數名稱:e。這樣一路簡化下來,結果就是「程式列表 1」的第 26 行了。
以下列出從匿名方法到最簡的 lambda expression 寫法,你不妨開啟 Visual Studio 2008,親自實驗一下各種寫法,應該會更有感覺。
Predicate<Employee> p1 = delegate(Employee e) { return e.Name.StartsWith("J"); }; Predicate<Employee> p2 = (e) => { return e.Name.StartsWith("J"); }; Predicate<Employee> p3 = e => { return e.Name.StartsWith("J"); }; Predicate<Employee> p4 = e => e.Name.StartsWith("J") ;
關於 lambda expression 的參數列,有些撰寫規則值得提一下:
- 如果有多個參數,就必須使用一對小括弧將它們包住。
- 如果不用傳遞參數,還是得寫一對空的小括弧:( )。
- 參數型別不見得都能省略。比如說,如果委派方法有 ref 或 out 參數,就還是得明白宣告型別。
向 LINQ 前進
前面的範例是從串列中找到其中一個元素,而如果要找到符合條件的多個元素,List<T> 還提供了 FindAll 方法。這對我們來說應該很簡單了,只要這麼寫:empList = empList.FindAll(e => e.Name.Contains("i"));
就能傳回所有姓名中包含字母 'i' 的員工了。等一下!當我在 Visual Studio 2008 的編輯器中輸入 "empList." 的時候,從 IntelliSense 提示的成員清單裡面看到好多新的方法,例如類似 SQL 指令的 Where、OrderBy、Select 等,這些方法的用途是甚麼?從哪裡來的呢?它們是擴充方法(extension methods),來自 System.Linq.Enumerable 類別。它們各自有兩個版本,這裡各列出一個原型宣告:
public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate ) public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector ) public static IEnumerable<TResult> Select<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, TResult> selector )
從原型宣告可以看出,它們要擴充的是 IEnumerable 介面。此外,這三個方法都需要傳入一個
Func<T, R> 型別的參數,這個 Func<T, R> 是定義於 System 命名空間的泛型委派型別,其中參數 T 代表傳入參數的型別,參數 R 則為傳回值的型別。 Func<...> 泛型委派有五個版本,最多可有四個 T,也就是最多可傳入四個參數。這表示甚麼?這表示 .NET 為我們預先定義了五種泛型委派,當你的委派方法需要一個傳回值,以及有零至四個參數時,就不一定要定義新的委派型別,你也可以直接使用現成的 Func<...>。
了解 Func<...> 型別的作用之後,接著試試修改「程式列表 1」的範例,先改用 Where 找出姓名包含字母 'i' 的員工,並將它們印出來。結果如「程式列表 2」所示。
程式列表 2:使用 Where 擴充方法尋找員工
1: public void Run()
2: {
3: var empList = new List<Employee>()
4: {
5: new Employee { Name = "Michael", Age = 20 },
6: new Employee { Name = "Douglas", Age = 25 },
7: new Employee { Name = "Jenifer", Age = 14 }
8: };
9:
10: IEnumerable<Employee> list = empList.Where<Employee>(e => e.Name.Contains("i"));
11:
12: foreach (Employee e in list)
13: {
14: Console.WriteLine(e.Name);
15: }
16: }
IEnumerable<Employee> list = empList.Where<Employee>(e => e.Name.Contains("i")) .OrderBy<Employee, int>(e => e.Age);再接再厲,這次加上 Select 方法,從找到的員工清單中只選出我們要的姓名欄位。結果如下:
var names = empList.Where<Employee>(e => e.Name.Contains("i")) .OrderBy<Employee, int>(e => e.Age) .Select<Employee, string>(e => e.Name);由於加上了 Select 方法,因此其傳回值不再是 IEnumerable<Employee>,而是
IEnumerable<string>。方便起見,這裡宣告成 var 隱含型別。
呼~到這裡應該差不多了。我們已經沾到 LINQ 的一點邊,再往下寫就嫌長了。之所以提到 Func<...> 委派型別以及 Where、OrderBy、Select 等擴充方法,主要也是為了學習 LINQ 做暖身。最後就把剛剛的 Where+OrderBy+Select 的版本改用 LINQ 表示式來寫,做個對照:
var names = from e in empList where e.Name.Contains("i") orderby e.Age select e.Name;經過上述練習,再看到 LINQ 查詢的表示式時,是不是覺得親切多了呢?
小結
這應該就是最近這幾篇 C# 筆記的最終回了。當初的目標就是「進入 LINQ」,因為 LINQ 技術其實已經有很多書籍和網路文章可以參考學習,但我覺得比較少人去詳細解釋為什麼它的語法會長這個樣子,以及這些查詢語法的背後,編譯器實際上會幫我們產生哪些方法呼叫。我想,若能先讓自己的腦袋把一些疑問釐清(先讓腦袋接受它),就比較容易跨過入門的這道門檻;而一旦跨過門檻,再搭配一本好書來學習 LINQ 的相關技術,應該就水到渠成了。相關文章
感謝分享
回覆刪除Thank you very much.
回覆刪除這對我幫助很大
一下子由C#1.0至C#3.0,實在不太習慣!
回覆刪除看了您的文章,讓小弟豁然開朗~~~
感恩, 受益良多
回覆刪除謝謝!很清楚的說明。
回覆刪除Tks
回覆刪除nice...
回覆刪除thx and god bless the world..
^__^