CodeGym /課程 /C# SELF /延遲執行

延遲執行

C# SELF
等級 34 , 課堂 0
開放

1. 前言

想像一下:你寫了一個超長又漂亮的 LINQ 查詢,心想——「搞定啦,馬上就會處理完!」結果發現什麼都沒發生,直到你去算元素數量或把結果轉成陣列。這不是 bug,是 feature 啦。

在 .NET 裡,LINQ 用的是延遲執行:查詢不會啟動,除非你真的開始取資料。就像一個很懶的服務生——不會衝去廚房,除非聽到:「拿點吃的來!」,雖然訂單早就下好了。

這種行為,也叫lazy evaluationLazy 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 結構實作。每次你開始遍歷集合(像呼叫 foreachToList()),查詢就會重新啟動。

重要觀察

如果你在定義查詢和執行查詢之間改變了原始集合,新的或改過的資料也會出現在結果裡。

範例:


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[結果或動作]
deferred execution 流程圖:查詢會「等」到真的被用到才動作

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 方法和執行模式

方法 延遲執行 馬上執行
Where
Select
Take
Skip
Join
ToList
ToArray
Count
First

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

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION