0Pricing
C# Academy · 课时

惰性求值语义

了解迭代器的延迟执行。

惰性求值语义 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

延迟执行

迭代器和 LINQ 查询采用延迟执行:定义 query 几乎不会做任何事情。只有实际逐项枚举结果时,工作才会发生。

枚举之前不会执行任何操作

调用迭代器方法不会运行其主体。只有 foreach(或其他消费者)提取第一个值时,主体才开始执行。

using System;
using System.Collections.Generic;

public class Program
{
    static IEnumerable<int> Numbers()
    {
        Console.WriteLine("-- iterator body started --");
        yield return 1;
        yield return 2;
    }

    public static void Main()
    {
        var seq = Numbers();
        Console.WriteLine("query created, body not run yet");
        foreach (var n in seq) Console.WriteLine(n);
    }
}

值会逐个生成

每次迭代只会执行迭代器中足以生成下一个值的部分。这种基于拉取的模型即使面对超大序列也能保持较低的内存占用。

using System;
using System.Collections.Generic;

public class Program
{
    static IEnumerable<int> Trace()
    {
        for (int i = 1; i <= 3; i++)
        {
            Console.WriteLine("producing " + i);
            yield return i;
        }
    }

    public static void Main()
    {
        foreach (var n in Trace())
            Console.WriteLine("consumed " + n);
    }
}

重新枚举会再次执行

延迟序列没有缓存的结果。枚举两次会执行迭代器两次;如果 source 发生变化或工作开销很大,这一点很重要。

using System;
using System.Collections.Generic;

public class Program
{
    static int _calls = 0;
    static IEnumerable<int> Counter()
    {
        _calls++;
        yield return _calls;
    }

    public static void Main()
    {
        var seq = Counter();
        foreach (var n in seq) Console.WriteLine(n);
        foreach (var n in seq) Console.WriteLine(n);
        Console.WriteLine("total runs: " + _calls);
    }
}

捕获实时状态

由于执行是延迟的,迭代器会在枚举时而非定义时读取 source。定义 query 后所做的更改都能被看到。

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        var list = new List<int> { 1, 2 };
        IEnumerable<int> doubled = Double(list);
        list.Add(3); // added before enumeration
        foreach (var n in doubled) Console.WriteLine(n);
    }

    static IEnumerable<int> Double(IEnumerable<int> nums)
    {
        foreach (var n in nums) yield return n * 2;
    }
}

强制立即执行

若要快照结果,请使用 ToList() 或 ToArray() 物化序列。这样会运行迭代器一次并存储这些值,使其与 source 后续更改脱钩。

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var list = new List<int> { 1, 2 };
        List<int> snapshot = Double(list).ToList(); // executed now
        list.Add(99);
        Console.WriteLine(string.Join(",", snapshot));
    }

    static IEnumerable<int> Double(IEnumerable<int> nums)
    {
        foreach (var n in nums) yield return n * 2;
    }
}

短路执行可节省工作量

因为消费者会惰性地拉取值,类似查找第一个匹配项的操作可以提前停止。生成者永远不会计算未被请求的值。

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    static IEnumerable<int> Numbers()
    {
        for (int i = 1; ; i++)
        {
            Console.WriteLine("checking " + i);
            yield return i;
        }
    }

    public static void Main()
    {
        int firstBig = Numbers().First(n => n > 3);
        Console.WriteLine("found " + firstBig);
    }
}

延迟抛出异常

迭代器中的异常不会在调用方法时抛出,而是在枚举到出错的行时抛出。如果您期待在前置阶段完成验证,这可能会让您感到意外。

using System;
using System.Collections.Generic;

public class Program
{
    static IEnumerable<int> Risky(int divisor)
    {
        yield return 10 / divisor; // throws only during enumeration
    }

    public static void Main()
    {
        var seq = Risky(0);
        Console.WriteLine("no error yet");
        try { foreach (var n in seq) Console.WriteLine(n); }
        catch (DivideByZeroException) { Console.WriteLine("caught during enumeration"); }
    }
}

惰性处理链的组合成本很低

链接惰性运算符不会让它们反复运行。每个值都会按需流经整个 pipeline 一次,因此效率高且节省内存。

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var result = Enumerable.Range(1, 1000000)
            .Where(n => n % 2 == 0)
            .Select(n => n * n)
            .Take(3);
        foreach (var n in result) Console.WriteLine(n);
    }
}

何时进行物化

在您将多次枚举、source 可能发生更改,或必须在释放资源前捕获结果时进行物化。否则保持惰性,以节省内存和工作量。

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var evens = Enumerable.Range(1, 10).Where(n => n % 2 == 0).ToList();
        // Safe to enumerate many times now
        Console.WriteLine("count: " + evens.Count);
        Console.WriteLine("sum: " + evens.Sum());
    }
}

亲自尝试

直接观察延迟执行:定义 query,修改 source,只有在枚举时,更改才会出现在结果中。

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var source = new List<int> { 1, 2, 3 };
        var query = source.Where(n => n > 1); // not executed yet
        source.Add(10);
        source.Remove(2);
        // Executes now, reflecting all changes
        Console.WriteLine(string.Join(", ", query));
    }
}

快速检查

思考延迟执行的行为。

回顾

惰性(延迟)求值只会在序列被枚举时运行。

  • 定义 query 不会执行任何工作;使用它才会执行。
  • 重新枚举会重新运行迭代器。
  • 所有 source 都会在枚举时读取,因此之后的更改可以被看到。
  • 异常会在枚举期间出现。
  • 需要时使用 ToList()/ToArray() 进行物化。

常见问题解答

「惰性求值语义」课时是免费的吗?

是的 — 「惰性求值语义」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「惰性求值语义」这节课中我会学到什么?

了解迭代器的延迟执行。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「惰性求值语义」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 yield return 的迭代器方法
  2. yield break 与提前终止
  3. 惰性求值语义
  4. 自定义可枚举类型
← 返回 C# Academy