yield 是 C# 中一个非常巧妙的语法糖,它的核心作用是实现“延迟执行(Lazy Evaluation)”。
简单来说,它允许你在一个方法中逐个返回数据,而不需要一次性把所有数据都计算好并塞进一个 List 里返回。
为了理解 yield,我们先看一个不使用 yield 的传统写法:
|
1 2 3 4 5 6 7 8 9 10 |
// 传统写法:一次性生成所有数据(批发) public List<int> GetNumbers(int count) { var list = new List<int>(); for (int i = 0; i < count; i++) { list.Add(i); // 把所有数字都塞进内存里的集合 } return list; // 最后一次性返回整个集合 } |
缺点:如果 count 是 100 万,这个方法会瞬间在内存中分配一个包含 100 万个元素的数组,造成极大的内存浪费。
现在看看使用 yield 的写法:
|
1 2 3 4 5 6 7 8 |
// yield 写法:按需生成数据(零售) public IEnumerable<int> GetNumbersYield(int count) { for (int i = 0; i < count; i++) { yield return i; // 每次吐出一个数字,然后“暂停”在这里 } } |
优点:调用这个方法时,它根本不会立刻执行循环。只有当外部真正去读取下一个数据时,它才会执行一次循环,吐出一个值,然后暂停。内存中永远只存在当前这一个数字。
假设你要读取一个 10GB 的日志文件,如果用 File.ReadAllLines(),内存直接撑爆。用 yield 可以逐行处理:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public IEnumerable<string> ReadLargeFile(string filePath) { using (var reader = new StreamReader(filePath)) { string line; while ((line = reader.ReadLine()) != null) { yield return line; // 读一行,吐出一行,内存占用极小 } } }
// 使用时: foreach (var line in ReadLargeFile("huge_log.txt")) { if (line.Contains("ERROR")) Console.WriteLine(line); } |
如果你想让你自己写的类支持 foreach 循环,就可以用 yield:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Countdown { private int _start; public Countdown(int start) { _start = start; }
public IEnumerator<int> GetEnumerator() { for (int i = _start; i >= 0; i--) { yield return i; } } }
// 使用时: var countdown = new Countdown(5); foreach (var num in countdown) { Console.WriteLine(num); // 输出 5, 4, 3, 2, 1, 0 } |
|
1 2 3 4 5 6 7 8 9 |
public IEnumerable<int> GetEvenNumbers(int max) { for (int i = 0; i <= max; i++) { if (i > 10) yield break; // 如果超过10,直接结束整个迭代
if (i % 2 == 0) yield return i; } } |
|
1 2 3 4 |
var query = GetNumbersYield(100); // 此时循环一次都没执行!
// 只有走到这里,循环才开始 foreach(var item in query) { ... } |
yield 的本质是状态机。C# 编译器在底层帮你把 yield 方法编译成了一个复杂的类,自动帮你保存了局部变量和执行位置。
一句话口诀:当数据量很大,或者数据是按需生成的,且你只需要用 foreach 去遍历它时,果断使用 yield return。