为什么需要 C# 中的 yield?惰性集合的秘密
想象一下:您正在编写一个方法,需要返回从 1 到 100 万的数字列表。首先想到的是创建一个 List<int>,填充它并返回。但如果调用代码只想要前 10 个元素呢?您仍然花费时间创建了 100 万个对象,占用了内存——而这一切都是徒劳的。
这时,C# 中的 yield 关键字登场了。这不仅仅是语法糖,更是创建惰性(lazy)序列的强大工具。在本文中,我们将通过真实示例深入解析 yield return 和 yield break,了解它们在底层的工作原理,并学会在日常开发中应用它们。
本教程基于 Stack Overflow 上一个真实问题改编,该问题获得了数千次浏览。我们将其改编为教育内容,添加了逐步的 C# 代码示例和实用的建议。
C# 中的 yield 是什么?简单来说
yield 是 C# 中的一个上下文关键字,它告诉编译器:“这个方法是一个迭代器”。当您编写 yield return 时,方法不会立即全部执行。相反,它会在调用代码请求时逐个返回元素。这被称为惰性执行。
与通常的 return 不同,return 会结束方法,而 yield return 会暂停方法,记住其状态,并返回当前元素。下次调用时,方法会从上次暂停的地方继续执行。
语法:yield return 和 yield break
C# 中有两种 yield 形式:
- yield return — 返回序列中的下一个元素。
- yield break — 立即终止迭代(类似于循环中的
break)。
使用 yield 的方法必须返回以下类型之一:
IEnumerable<T>IEnumerableIAsyncEnumerable<T>(从 C# 8.0 开始)IEnumerator<T>或IEnumerator
第一个示例:从简单到复杂
让我们编写一个最简单的迭代器,生成从 1 到 5 的数字:
public static IEnumerable<int> GetNumbers(){ yield return 1; yield return 2; yield return 3; yield return 4; yield return 5;}
// 使用:foreach (int number in GetNumbers()){ Console.WriteLine(number);}输出:
12345乍一看,这类似于普通列表。但区别巨大:每个元素仅在 foreach 请求时才生成。无需为整个列表分配内存!
底层是如何工作的?
C# 编译器将包含 yield 的方法转换为一个完整的状态机(state machine)。它创建一个实现 IEnumerable<T> 和 IEnumerator<T> 的类。这个类存储:
- 当前状态(我们停在了方法的哪一行)。
- 方法的局部变量。
- 对当前对象的引用(
this),如果方法是非静态的。
每次调用 MoveNext()(由 foreach 执行)时,状态机会从上次停止的地方恢复执行。
真实示例:读取大文件
假设您需要处理一个 10 GB 的日志文件。如果一次性将整个文件读入内存,程序会因 OutOfMemoryException 而崩溃。使用 yield,您可以逐行读取文件并即时处理每一行:
public static IEnumerable<s