這是《C# 本事》LINQ 之章的第 2 篇摘錄,主要討論的議題是 LINQ 的延遲執行。

以下開始摘錄內容...

延遲執行

LINQ To Objects(以及其他 LINQ 提供者)有一個很重要的特性,叫做「延遲執行」(deferred execution),或稱為惰性求值(lazy evaluation)。顧名思義,就是在真正需要取用查詢結果的時候,才去執行查詢表示式。

參考底下的簡單範例:

static void Main()
{
    var numbers = new List<int> { 1, 2, 3 };
    
    IEnumerable<int> numberQuery = numbers.Select(num => num * 10); // 建立查詢

    numbers.Remove(2);  // 移除「來源集合」中的元素 2

    foreach (var num in numberQuery)    // 這裡才會執行查詢表示式
    {
        Console.Write(num + " ");   // 輸出 "10 30"
    }
}


此範例是使用 LINQ 擴充方法來建立查詢,其中使用了 `Select()` 將集合中的每個元素轉換成另一個數值(稍後會進一步介紹 Select 的用法)。
  • 第 5 行:針對整數陣列 numbers 建立查詢時,僅使用了 Select() 方法來進行投射,而投射的結果所返回的物件會是一個 IEnumerable<int> 序列。傳入 Select() 方法的委派會將元素值乘以 10 之後再回傳。
  • 第 7 行:將整數串列 numbers 裡面的元素 2 移除。
  • 第 9 行:此迴圈在巡覽 numberQuery 序列時會執行查詢。

請注意,如果第 5 行所建立的查詢是立刻執行的話,最後輸出在螢幕上的結果應該會是 "10 20 30"。然而,由於延遲執行查詢的緣故,須等到程式執行至第 9 行的迴圈時,才會真正執行先前所建立的查詢;此時由於查詢的資料來源 numbers 串列中的第二個元素已經被移除,故最終輸出的結果是 "10 30"。

由此可見,LINQ 的延遲執行,基本上具備兩個性質:
  • 「建立查詢」與「執行查詢」的動作是分開的。
  • 一旦需要讀取序列中的第一個元素,便會執行查詢。

因此,當你對一個 IEnumerable<T> 序列進行下列操作時,便會啟動查詢:
  • 傳回單一元素或數值的操作,例如 Count()、First()、Max() 等等。
  • 有內容轉換有關的的操作,例如:ToArray()、ToList()、ToDictionary()、ToLookup()。

重複求值

要特別注意的是,LINQ 的延遲執行在某些場合反而會造成重複求值(evaluation)的問題。請看底下的範例:

static void Main()
{
    var numbers = new List<int> { 1, 2, 3 };

    IEnumerable<int> numberQuery = numbers.Select(num => num); // 建立查詢

    for (int i = 1; i <= 3; i++)
    {
        Console.WriteLine($"第 {i} 次迴圈, 共 {numberQuery.Count()} 個元素");
        numbers.RemoveAt(0);  // 注意這裡移除的是來源串列中的元素
    }
}

你可以看到,由於延遲執行的緣故,每一次呼叫 `Count()` 方法就會執行一次查詢,所以每次的查詢結果都會隨著資料來源內容的變化而不同。這種情形通常不是我們想要的。

一般而言,我們會先把查詢結果轉換成串列(或陣列),然後才去改變串列中的元素。像這樣:

你可以看到,由於延遲執行的緣故,每一次呼叫 `Count()` 方法就會執行一次查詢,所以每次的查詢結果都會隨著資料來源內容的變化而不同。這種情形通常不是我們想要的。 一般而言,我們會先把查詢結果轉換成串列(或陣列),然後才去改變串列中的元素。像這樣:

    var numbers = new List<int> { 1, 2, 3 };
    var numberQuery = numbers.Select(num => num); // 建立查詢。
    var numberList = numberQuery.ToList();  // 執行查詢,並將查詢結果轉成一個串列。
    numberList.Remove(0); // 往後只存取這個串列,而不再去操作 numberQuery,以避免重複執行查詢。

小測驗

底下的程式碼執行完畢之後,螢幕上會輸出什麼?

   var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
   var query = numbers
               .Where(n => n > 3)
               .Select(n =>
               {
                   Console.WriteLine(n);
                   return n;
               });

答案是沒有輸出任何東西。若你對此答案心存疑慮,請回頭複習〈延遲執行〉一節的內容。

下回預告:LINQ API 實務練習

工商時間:購書請至《C# 本事》電子書主頁 Orz Orz

(如果是第一次在 leanpub 買書,請參考這篇:在 leanpub.com 買書的步驟


Happy learning!