iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 20 篇

Day 20|Deferred Execution:LINQ 查詢到底什麼時候執行?

  • 分享至 

  • xImage
  •  

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 的執行過程。


1. Deferred Execution:建立查詢不等於執行

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。


2. Multiple Enumeration:同一個查詢使用兩次會怎樣?

延續剛才的 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 就可能重複執行相同工作。

因此要記住:

同一個延遲查詢被列舉多次,查詢邏輯也可能重新執行多次。


3. 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 直接讀取已建立的結果

這就是:

延遲查詢

和:

已建立結果

最重要的差別。


4. 來源資料改變,結果會怎樣?

現在再看 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。


5. 回到 Day 19 的 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 什麼時候執行?

6. Immediate Execution:不是所有 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

不需要繼續檢查剩下的資料。


7. 實務上什麼時候使用 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(),取決於你需要「延遲查詢」還是「現在建立結果」。


今天最重要的 Mental Model

建立 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 時
又會發生什麼?

並說明原因。


練習二|Multiple Enumeration

將同一個 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

最後會得到什麼資料?

為什麼結果不同?


Day 20 小結

今天最重要的觀念:

情況 發生什麼事?
Where(...) 建立延遲查詢
foreach 開始列舉並執行查詢
同一個 query 列舉兩次 查詢可能重新執行
ToList() 列舉查詢並建立 List

另外:

Deferred Execution
→ 建立查詢時不立即處理所有資料

Multiple Enumeration
→ 重複列舉可能重新執行查詢

ToList()
→ 現在執行查詢並建立結果

Immediate Execution
→ 呼叫時就需要取得實際結果

Day 19 我們學的是:

LINQ
→ 要對資料做什麼

Lambda
→ 使用什麼規則

Day 20 則進一步理解:

LINQ
→ 什麼時候真正執行?

最後記住:

建立 LINQ 查詢,不一定代表查詢已經執行。

以及:

理解「何時列舉、何時建立結果」,才能真正掌握 LINQ 的執行方式。


上一篇
Day 19|LINQ 基礎:用一致的方式查詢 Collection
下一篇
Day 21|Exception Handling:程式發生錯誤時怎麼處理?
系列文
現在就學C# 與 ASP.NET Core 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言