1. 前言
想像一下:你寫了一個超長又漂亮的 LINQ 查詢,心想——「搞定啦,馬上就會處理完!」結果發現什麼都沒發生,直到你去算元素數量或把結果轉成陣列。這不是 bug,是 feature 啦。
在 .NET 裡,LINQ 用的是延遲執行:查詢不會啟動,除非你真的開始取資料。就像一個很懶的服務生——不會衝去廚房,除非聽到:「拿點吃的來!」,雖然訂單早就下好了。
這種行為,也叫lazy evaluation(Lazy Evaluation),讓 LINQ 在處理超大、甚至無限或很吃資源的資料來源時特別有效率。
延遲執行意思是,LINQ 查詢定義好後不會馬上跑。只有當你真的開始遍歷或查看集合裡的資料時,它才會動作。
範例說明
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// LINQ 查詢
var query = numbers.Where(n =>{
Console.WriteLine($"檢查 {n}");
return n % 2 == 0;
});
Console.WriteLine("查詢已經定義,但數字還沒被檢查!");
// 只有現在才開始遍歷!
foreach (var n in query)
{
Console.WriteLine($"找到偶數: {n}");
}
會發生什麼事?
當你執行這段程式時,你會發現,在 foreach 之前,console 上什麼都沒印出來——連 Where 裡的條件都沒跑。只有當你開始遍歷元素(像用 foreach),查詢才會開始執行。
這就是 deferred execution——沒叫就沒人動!
2. 為什麼要延遲執行?
延遲執行讓程式不只優雅,還超有效率。為什麼要提前做一堆事,搞不好結果根本用不到?這在你面對超大集合或資料流時特別重要——想像一個可能無限的來源。一次全載進記憶體根本不合理。
而且,延遲執行讓你可以隨意組合、串接查詢。你可以疊一堆 LINQ 操作,不用擔心它們會馬上執行。只有當你真的要資料時,才會動作——早一秒都不會。
比喻
延遲執行就像手機裡的購物清單:你可以一直加、改商品,但只有你真的要去買東西時(進店裡),才會用到這個清單。
3. 底層怎麼運作?
LINQ 查詢如果回傳 IEnumerable<T>,通常是用 iterator(yield return)或特殊 lazy 結構實作。每次你開始遍歷集合(像呼叫 foreach 或 ToList()),查詢就會重新啟動。
重要觀察
如果你在定義查詢和執行查詢之間改變了原始集合,新的或改過的資料也會出現在結果裡。
範例:
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);
numbers.Add(4); // 加入新數字
foreach (var n in query)
{
Console.WriteLine(n); // 會印出 2, 3, 4
}
流程圖:LINQ 查詢什麼時候執行?
flowchart TD
A[定義 LINQ 查詢] --> B{執行查詢?}
B -- 否 --> C[等待]
B -- 是(像 foreach, ToList) --> D[執行查詢]
D --> E[結果或動作]
4. LINQ「懶惰」很有用的例子
懶惰過濾
var bigNumbers = Enumerable.Range(1, 1_000_000_000)
.Where(n => n % 123_456 == 0);
foreach (var n in bigNumbers.Take(5))
{
Console.WriteLine(n);
}
這裡發生什麼?
查詢創造了一個可能有十億個元素的集合,但實際只會過濾並回傳 5 個數字!其他的根本不會算,也不會佔記憶體。
巢狀查詢和交易
假設我們有訂單和商品清單,要找出包含特定商品的前 5 筆訂單。
var orders = GetBigOrderList(); // 假設這裡有成千上萬筆訂單
var filtered = orders
.Where(order => order.Products.Any(p => p.Name == "咖啡"))
.Take(5);
foreach(var o in filtered)
{
Console.WriteLine(o.Id);
}
LINQ 找到 5 筆符合的訂單後,剩下的根本不會看!
5. 意外狀況
有些情境下,延遲執行會讓你遇到意想不到的事:
查詢定義後資料來源被改
如前面例子,如果你在查詢定義和實際執行之間改變了原始集合,查詢會看到新資料。
同一個查詢被遍歷多次
LINQ 查詢每次遍歷都會重新執行。
var query = numbers.Where(n => {
Console.WriteLine($"檢查 {n}");
return n % 2 == 0;
});
foreach(var n in query) {} // 第一次遍歷
foreach(var n in query) {} // 第二次遍歷——又全部重算一次
如果你想要重複用同樣的結果——就要 materialize(像 .ToList() 或 .ToArray())。
6. 哪些 LINQ 查詢不是延遲的?
不是所有 LINQ 操作都會延遲。有些方法會馬上(「貪婪」)執行(Immediate Execution)。例如:
- .ToList()
- .ToArray()
- .Count()
- .Average()
- .Sum()
- .First(), .Last(), .Single()
這些都會讓 LINQ 查詢馬上執行,因為它們回傳的不是 IEnumerable,而是直接的結果。
範例:
var query = numbers.Where(n => n > 2);
var result = query.ToList(); // 這裡查詢馬上執行!
7. 「lazy evaluation」概念(Lazy Evaluation)
延遲執行(deferred execution)就是 .NET 裡 lazy evaluation 的一種。
lazy evaluation 就是結果不會算,除非真的需要。除了 LINQ,C# 還有其他 lazy evaluation 的機制。
Lazy<T> 類別
C# 有個特別的型別 Lazy<T>,可以讓你「需要時才產生」值。
簡單範例:
// 建立一個 lazy 數字,只有你真的要時才會算
var lazyValue = new Lazy<int>(() =>
{
Console.WriteLine("計算值!");
return 42;
});
Console.WriteLine("Lazy 物件已建立,但值還沒算出來");
Console.WriteLine($"值: {lazyValue.Value}"); // 這裡才會計算
這有什麼用?
比如你有個參數很少用到,而且計算很久或很耗資源。
8. 表格:LINQ 方法和執行模式
| 方法 | 延遲執行 | 馬上執行 |
|---|---|---|
|
✅ | ❌ |
|
✅ | ❌ |
|
✅ | ❌ |
|
✅ | ❌ |
|
✅ | ❌ |
|
❌ | ✅ |
|
❌ | ✅ |
|
❌ | ✅ |
|
❌ | ✅ |
9. 常見錯誤和實作細節
「重複遍歷造成多餘運算」
因為延遲執行每次遍歷都會重新計算,有時會多做很多次一樣的事。
var expensiveQuery = bigList.Where(x => SomeHeavyCalculation(x));
var result1 = expensiveQuery.ToList(); // 算一次
var result2 = expensiveQuery.ToList(); // 又算一次(結果一樣但又花時間)
解法:只 materialize 一次資料:
var cached = expensiveQuery.ToList();
「try catch 也抓不到」
如果查詢裡的 function 丟出例外,只有在遍歷集合時才會發生,不是在查詢定義時。
遍歷時修改集合
遍歷集合時去改它,會丟出 InvalidOperationException。
GO TO FULL VERSION