《C# 本事》摘錄:LINQ (2)
文章目錄
這是《C# 本事》LINQ 之章的第 2 篇摘錄,主要討論的議題是 LINQ 的延遲執行。
以下開始摘錄內容...
延遲執行
LINQ To Objects(以及其他 LINQ 提供者)有一個很重要的特性,叫做「延遲執行」(deferred execution),或稱為惰性求值(lazy evaluation)。顧名思義,就是在真正需要取用查詢結果的時候,才去執行查詢表示式。
參考底下的簡單範例:
此範例是使用 LINQ 擴充方法來建立查詢,其中使用了 `Select()` 將集合中的每個元素轉換成另一個數值(稍後會進一步介紹 Select 的用法)。
請注意,如果第 5 行所建立的查詢是立刻執行的話,最後輸出在螢幕上的結果應該會是 "10 20 30"。然而,由於延遲執行查詢的緣故,須等到程式執行至第 9 行的迴圈時,才會真正執行先前所建立的查詢;此時由於查詢的資料來源 numbers 串列中的第二個元素已經被移除,故最終輸出的結果是 "10 30"。
由此可見,LINQ 的延遲執行,基本上具備兩個性質:
因此,當你對一個 IEnumerable<T> 序列進行下列操作時,便會啟動查詢:
重複求值
要特別注意的是,LINQ 的延遲執行在某些場合反而會造成重複求值(evaluation)的問題。請看底下的範例:
你可以看到,由於延遲執行的緣故,每一次呼叫 `Count()` 方法就會執行一次查詢,所以每次的查詢結果都會隨著資料來源內容的變化而不同。這種情形通常不是我們想要的。
一般而言,我們會先把查詢結果轉換成串列(或陣列),然後才去改變串列中的元素。像這樣:
你可以看到,由於延遲執行的緣故,每一次呼叫 `Count()` 方法就會執行一次查詢,所以每次的查詢結果都會隨著資料來源內容的變化而不同。這種情形通常不是我們想要的。 一般而言,我們會先把查詢結果轉換成串列(或陣列),然後才去改變串列中的元素。像這樣:
小測驗
底下的程式碼執行完畢之後,螢幕上會輸出什麼?
答案是沒有輸出任何東西。若你對此答案心存疑慮,請回頭複習〈延遲執行〉一節的內容。
下回預告:LINQ API 實務練習
工商時間:購書請至《C# 本事》電子書主頁 Orz Orz
(如果是第一次在 leanpub 買書,請參考這篇:在 leanpub.com 買書的步驟)
Happy learning!
以下開始摘錄內容...
延遲執行
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!