Day 19 我們學會使用 LINQ 查詢 Collection。
例如,要找出 Quantity > 10 的訂單:
List<Order> result =
orders
.Where(
order => order.Quantity > 10
)
.ToList();
我們知道:
Where()
→ 篩選資料
Lambda
→ 提供篩選條件
ToList()
→ 建立 List
但如果拿掉 ToList():
var query =
orders.Where(
order => order.Quantity > 10
);
這時就產生兩個重要問題:
Where()寫完之後,資料已經篩選完成了嗎?
如果同一個 query 使用兩次,篩選條件會不會重新執行?
今天要理解的核心就是:
建立 LINQ 查詢,和真正執行查詢,可能發生在不同的時間點。
這個特性稱為:
Deferred Execution
→ 延遲執行
Deferred Execution
→ 建立查詢與執行查詢的差別
Enumeration
→ 什麼時候開始取得資料?
Multiple Enumeration
→ 同一個查詢列舉兩次會怎樣?
ToList()
→ 什麼時候真正建立結果?
來源資料變更
→ 為什麼查詢結果可能改變?
Immediate Execution
→ 哪些 LINQ Method 呼叫時就取得結果?
今天最重要的觀念:
建立查詢
≠
執行查詢
本篇以記憶體中的 Collection,也就是 LINQ to Objects 為例,直接觀察 LINQ 的執行過程。
Deferred Execution(延遲執行) 可以先理解成:
建立查詢時,不一定立即處理所有資料;真正需要資料時,才開始執行查詢。
為了清楚觀察,我們先使用簡單的數字:
List<int> numbers =
new List<int>
{
1,
2,
3,
4,
5
};
建立查詢:
var query =
numbers.Where(
number => number > 3
);
看到這段程式,很容易直覺認為:
query
→ 已經得到 4、5
但這時 Where() 還沒有逐筆執行:
number > 3
目前比較接近:
numbers
↓
建立 Where 查詢
↓
保存條件:
number > 3
也就是:
目前建立的是「怎麼找資料」的查詢,而不是已經建立完成的結果 List。
Console.WriteLine() 驗證把輸出放進 Lambda:
var query =
numbers.Where(
number =>
{
Console.WriteLine(
$"檢查:{number}"
);
return number > 3;
}
);
Console.WriteLine("查詢已建立");
執行後只有:
查詢已建立
並沒有:
檢查:1
檢查:2
檢查:3
...
這表示:
Where()
→ 已經建立查詢
Lambda
→ 還沒有逐筆執行
foreach接著:
foreach (int number in query)
{
Console.WriteLine(
$"符合:{number}"
);
}
這時才會輸出:
檢查:1
檢查:2
檢查:3
檢查:4
符合:4
檢查:5
符合:5
實際流程:
foreach 需要資料
↓
開始列舉 query
↓
Where 逐筆取得資料
↓
執行 Lambda
↓
number > 3 ?
↓
true
→ 交給 foreach
這種逐筆取得資料進行處理的過程稱為:
Enumeration
→ 列舉
因此:
對這個 LINQ to Objects 的
Where()查詢而言,建立查詢時不會立即逐筆篩選;真正列舉時,才開始執行 Lambda。
這就是 Deferred Execution。
延續剛才的 query:
var query =
numbers.Where(
number =>
{
Console.WriteLine(
$"檢查:{number}"
);
return number > 3;
}
);
先列舉第一次:
Console.WriteLine("第一次列舉");
foreach (int number in query)
{
Console.WriteLine(
$"結果:{number}"
);
}
接著再列舉一次:
Console.WriteLine("第二次列舉");
foreach (int number in query)
{
Console.WriteLine(
$"結果:{number}"
);
}
執行結果:
第一次列舉
檢查:1
檢查:2
檢查:3
檢查:4
結果:4
檢查:5
結果:5
第二次列舉
檢查:1
檢查:2
檢查:3
檢查:4
結果:4
檢查:5
結果:5
可以發現:
第一次 foreach
→ 檢查 1~5
第二次 foreach
→ 又檢查一次 1~5
這種對同一個查詢重複列舉的情況稱為:
Multiple Enumeration
→ 多次列舉
為什麼會重新檢查?
因為:
延遲查詢不會因為第一次列舉完成,就自動把查詢結果保存起來。
所以:
第一次列舉
→ 執行一次查詢
第二次列舉
→ 再執行一次查詢
重點不是:
結果輸出了兩次
而是:
Lambda
也執行了兩輪
如果查詢本身成本較高,Multiple Enumeration 就可能重複執行相同工作。
因此要記住:
同一個延遲查詢被列舉多次,查詢邏輯也可能重新執行多次。
ToList():把目前結果建立起來如果我們希望:
現在執行查詢
↓
建立目前結果
↓
後面重複使用同一份結果
可以使用:
ToList()
例如:
List<int> result =
numbers
.Where(
number => number > 3
)
.ToList();
可以先理解成:
numbers
↓
Where()
建立查詢
↓
ToList()
開始列舉
↓
執行 Where
↓
建立 List<int>
↓
result = [4, 5]
所以:
ToList()會列舉查詢,並把當下取得的資料建立成新的List<T>。
這種把查詢結果真正建立成集合的過程,也常稱為 Materialization。
目前知道這個名稱即可。
ToList()List<int> result =
numbers
.Where(
number =>
{
Console.WriteLine(
$"檢查:{number}"
);
return number > 3;
}
)
.ToList();
執行到 ToList() 時:
檢查:1
檢查:2
檢查:3
檢查:4
檢查:5
最後得到:
result = [4, 5]
接著:
Console.WriteLine("第一次列舉");
foreach (int number in result)
{
Console.WriteLine(number);
}
Console.WriteLine("第二次列舉");
foreach (int number in result)
{
Console.WriteLine(number);
}
結果:
第一次列舉
4
5
第二次列舉
4
5
不會再次看到:
檢查:1
檢查:2
...
因為現在列舉的是:
已經建立完成的 List<int>
而不是重新執行原本的 Where() 查詢。
query 和 ToList() 的差別| 操作 | 發生什麼事 |
|---|---|
Where(...) |
建立延遲查詢 |
第一次列舉 query |
執行查詢 |
第二次列舉 query |
再次執行查詢 |
ToList() |
列舉查詢並建立 List |
| 重複列舉 List | 直接讀取已建立的結果 |
這就是:
延遲查詢
和:
已建立結果
最重要的差別。
現在再看 Deferred Execution 很重要的一個特性。
先建立:
List<int> numbers =
new List<int>
{
1,
2,
3
};
var query =
numbers.Where(
number => number > 2
);
建立 query 後,再新增:
numbers.Add(4);
接著才列舉:
foreach (int number in query)
{
Console.WriteLine(number);
}
結果:
3
4
為什麼 4 也出現了?
因為:
建立 query
↓
尚未執行查詢
numbers.Add(4)
↓
來源變成 [1, 2, 3, 4]
foreach
↓
此時才開始執行查詢
所以:
延遲查詢會在列舉時讀取來源,因此列舉之前的來源變化,可能反映在查詢結果中。
ToList() 呢?改成:
List<int> numbers =
new List<int>
{
1,
2,
3
};
List<int> result =
numbers
.Where(
number => number > 2
)
.ToList();
numbers.Add(4);
ToList() 執行時:
numbers = [1, 2, 3]
所以建立:
result = [3]
之後:
numbers.Add(4);
雖然來源變成:
numbers = [1, 2, 3, 4]
但:
foreach (int number in result)
{
Console.WriteLine(number);
}
結果仍然是:
3
因為 result 已經先建立完成。
| 比較 | 延遲 query | ToList() 後的結果 |
|---|---|---|
| 查詢何時執行? | 列舉時 | 呼叫 ToList() 時 |
| 來源新增資料 | 後續列舉可能反映變化 | 已建立的 List 不會自動新增 |
| 重複列舉 | 查詢可能重新執行 | 直接讀取 List |
一句話記住:
延遲查詢使用列舉當下的來源;
ToList()則把當下的查詢結果建立成 List。
Order現在回到昨天的範例。
先建立:
var query =
orders.Where(
order => order.Quantity > 10
);
接著新增:
orders.Add(
new Order(
"A006",
"TXF",
30,
1800m
)
);
最後才:
List<Order> result =
query.ToList();
問題:
A006 會不會出現在 result?
答案是:
會
原因很簡單:
建立 query
↓
尚未執行
新增 A006
↓
orders 改變
ToList()
↓
此時才執行查詢
此時:
A006.Quantity = 30
符合:
Quantity > 10
因此會出現在結果中。
這就是 Day 19 和 Day 20 的關係:
Day 19
LINQ 怎麼寫?
↓
Day 20
LINQ 什麼時候執行?
不是所有 LINQ Method 都只是在建立延遲查詢。
有些 Method 呼叫時就必須取得實際結果。
例如 Day 19 學過:
bool exists =
numbers.Any(
number => number > 3
);
Any() 必須直接回答:
有沒有?
所以會得到:
true
或
false
這類呼叫時就需要取得結果的操作,可以先理解成:
Immediate Execution
→ 呼叫時取得結果
常見比較:
| Method | 執行特性 | 結果 |
|---|---|---|
Where() |
Deferred | 一組查詢結果 |
Select() |
Deferred | 一組查詢結果 |
OrderBy() |
Deferred | 排序後的查詢結果 |
Any() |
呼叫時取得結果 | bool |
Count() |
呼叫時取得結果 | int |
FirstOrDefault() |
呼叫時取得結果 | 單筆 / Default |
ToList() |
呼叫時列舉 | List<T> |
但要注意:
呼叫時取得結果,不代表一定要檢查所有資料。
例如:
bool exists =
numbers.Any(
number => number > 3
);
只要找到第一筆符合條件的資料:
4
就已經可以得到:
true
不需要繼續檢查剩下的資料。
ToList()?Day 19 的 LINQ 範例常看到:
.ToList()
但今天要補上一個重要觀念:
ToList()不是每個 LINQ 查詢固定都要加上的結尾。
應該先問:
我現在需要的是延遲查詢,還是一份已經建立完成的結果?
var query =
numbers.Where(
number => number > 3
);
foreach (int number in query)
{
Console.WriteLine(number);
}
如果只需要使用一次,不一定要先呼叫:
ToList()
List<int> result =
numbers
.Where(
number => number > 3
)
.ToList();
如果需求是:
現在執行查詢
↓
建立目前結果
↓
後續使用這份 List
就可以使用 ToList()。
如果:
同一個延遲 query
↓
後面會重複列舉
而查詢本身又有一定成本,可以考慮先:
ToList()
把結果建立起來。
但不要理解成:
ToList()永遠比較快。
因為 ToList() 本身也需要:
執行查詢
+
建立 List
+
保存結果
因此:
是否使用
ToList(),取決於你需要「延遲查詢」還是「現在建立結果」。
建立 Query
↓
尚未執行查詢
↓
開始 Enumeration
↓
執行查詢
↓
需要保存結果?
↙ ↘
否 是
↓ ↓
直接使用 ToList()
↓
List<T>
另外:
同一個延遲 query
↓
列舉兩次
↓
查詢可能執行兩次
所以整篇最核心的觀念就是:
建立查詢
≠
執行查詢
使用:
List<int> numbers =
new List<int>
{
1,
2,
3,
4,
5
};
完成以下練習。
建立:
var query =
numbers.Where(
number =>
{
Console.WriteLine(
$"檢查:{number}"
);
return number > 3;
}
);
觀察:
建立 query 時
有沒有出現「檢查」?
foreach 時
又會發生什麼?
並說明原因。
將同一個 query 列舉兩次:
foreach (int number in query)
{
Console.WriteLine(number);
}
foreach (int number in query)
{
Console.WriteLine(number);
}
觀察:
Lambda 總共執行幾輪?
接著:
List<int> result =
query.ToList();
再列舉 result 兩次。
比較:
query
與:
result
的差異。
先建立:
var query =
numbers.Where(
number => number > 3
);
List<int> result =
query.ToList();
接著:
numbers.Add(10);
再分別查看:
query
和:
result
最後會得到什麼資料?
為什麼結果不同?
今天最重要的觀念:
| 情況 | 發生什麼事? |
|---|---|
Where(...) |
建立延遲查詢 |
foreach |
開始列舉並執行查詢 |
| 同一個 query 列舉兩次 | 查詢可能重新執行 |
ToList() |
列舉查詢並建立 List |
另外:
Deferred Execution
→ 建立查詢時不立即處理所有資料
Multiple Enumeration
→ 重複列舉可能重新執行查詢
ToList()
→ 現在執行查詢並建立結果
Immediate Execution
→ 呼叫時就需要取得實際結果
Day 19 我們學的是:
LINQ
→ 要對資料做什麼
Lambda
→ 使用什麼規則
Day 20 則進一步理解:
LINQ
→ 什麼時候真正執行?
最後記住:
建立 LINQ 查詢,不一定代表查詢已經執行。
以及:
理解「何時列舉、何時建立結果」,才能真正掌握 LINQ 的執行方式。