C# 筆記:從 Lambda 表示式到 LINQ

之前的<C# 筆記:重訪委派--從 C# 1.0 到 2.0 到 3.0>已經交代過 lambda expressions 的語法及其來龍去脈,這篇筆記會先複習一下 lambda 表示式,然後進入 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 表示式的寫法,主要有兩個動作:
  1. 把關鍵字 delegate 去掉。
  2. 加上 => 運算子。這個運算子的左邊要放匿名方法的參數宣告,右邊則是匿名方法的本體。=> 的意思是 "goes to",亦即「丟到」、「傳入」的意思。
照這兩個基本步驟,範例程式的第 26 行應該會長這樣:

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:  }
接著加上 OrderBy,把找到的員工清單以年齡排序。由於 Where 傳回的是 IEnumerable<T>,所以我們可以用串接的方式,直接把 OrderBy 呼叫接在後面。結果原本的第 10 行變成這樣:

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 的相關技術,應該就水到渠成了。

相關文章

Post Comments

技術提供:Blogger.