CodeGym /课程 /C# SELF /延迟执行

延迟执行

C# SELF
第 34 级 , 课程 0
可用

1. 入门

想象一下:你写了个又长又帅的LINQ查询,心想——“哇哦,马上就处理完啦!”结果发现啥都没发生,除非你数一下元素或者把结果转成数组。这不是bug,是feature。

在.NET里,LINQ用的是延迟执行:查询不会立刻跑,只有你真的开始取数据时才会动手。这就像个懒服务员——你不说“上菜!”,他绝不会冲进厨房,哪怕你单都下好了。

这种行为叫懒加载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循环之前,控制台啥都不会输出——连你Where里的条件都没执行。只有你开始遍历元素(比如用foreach),查询才会真正执行。

这就是deferred execution——你不喊,没人干活!

2. 为什么要用延迟执行?

延迟执行让代码不仅优雅,还超高效。为啥要提前干活,说不定结果根本用不上呢?这在你面对超大集合或者数据流(比如可能无限的来源)时尤其重要。一次性全加载进内存,简直不现实。

而且,延迟执行让你可以随意组合、叠加查询。你能随便拼LINQ操作链,不用担心它们会立刻执行。只有你真的要数据时,才会动手——早一秒都不会。

类比

延迟执行就像手机里的购物清单:你可以随便加、改商品,但只有你真要去超市买东西时,才会用到这个清单(也只有那时才会处理它)。

3. 底层是怎么实现的?

返回IEnumerable<T>的LINQ查询,通常用迭代器(yield return)或者专门的懒加载结构实现。每次你开始遍历集合(比如用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[结果或动作]
延迟执行流程图:查询“等着”直到你真的用它

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) {} // 第二次遍历——又重新算一遍

如果你想要重复用同样的结果——就要物化(比如.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

延迟执行(deferred execution)其实就是.NET里懒加载(Lazy Evaluation)的一个具体例子。

懒加载就是结果只有在你真的需要时才会被计算。除了LINQ,C#里还有别的懒加载机制。

Lazy<T>

C#有个专门的Lazy<T>类型,可以按需创建值。

简单例子:


// 创建一个懒加载的数字,只有你要它时才会计算
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(); // 又算了一次(结果一样,但又浪费时间)

解决方法:只物化一次数据:
var cached = expensiveQuery.ToList();

“异常只有遍历时才抛”

如果查询里的函数抛异常,只有遍历集合时才会抛,而不是定义查询时。

遍历时修改集合

遍历集合时修改它,可能会抛出InvalidOperationException异常。

2
任务
C# SELF, 第 34 级, 课程 0
已锁定
延迟执行与多次查询
延迟执行与多次查询
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION