文章目录
- `yield` 生成的迭代器 和 手动实现的迭代器 不一样吗?
- 深入解析 `yield` 与 `List` 的本质区别
- C# `yield` 关键字使用示例详解
- 实际项目中 `yield` 的典型应用场景与代码示例
yield
生成的迭代器 和 手动实现的迭代器 不一样吗?
yield
生成的迭代器和手动实现的迭代器在功能上等价(都实现了 IEnumerable
/IEnumerator
接口),但底层实现和开发体验有显著差异:
1. 本质区别对比
特性 | yield 生成的迭代器 | 手动实现的迭代器 |
---|---|---|
实现方式 | 编译器自动生成状态机类 | 手动编写完整 IEnumerator 实现 |
代码复杂度 | 只需写业务逻辑(如 for+yield return ) | 需实现 MoveNext() 、Current 等所有方法 |
内存管理 | 自动保存/恢复局部变量状态 | 需手动管理状态(如字段记录当前位置) |
可读性 | 接近普通方法的直观写法 | 需要理解迭代器模式的设计 |
执行控制 | 受限(不能完全控制状态机) | 完全可控(可自定义每一步行为) |
2. 底层实现解析
(1) yield
版本(编译器魔法)
IEnumerable<int> GetNumbers() {for (int i = 1; i <= 5; i++) {yield return i;}
}
▶ 编译器生成的等价代码(简化版):
class GeneratedEnumerator : IEnumerator<int> {private int _state;private int _current;private int _i; // 自动保存循环变量i的状态public bool MoveNext() {switch (_state) {case 0: _i = 1; _state = 1; goto case 1;case 1:if (_i > 5) return false;_current = _i++;_state = 1; // 保持状态return true;}return false;}// 其他接口实现...
}
(2) 手动实现版本
class ManualEnumerator : IEnumerator<int> {private int[] _data = { 1, 2, 3, 4, 5 };private int _index = -1;public bool MoveNext() {_index++;return _index < _data.Length;}public int Current => _data[_index];// 其他接口实现...
}
3. 关键差异点
(1) 状态管理方式
• yield
:自动将局部变量(如 i
)提升为生成类的字段,保存完整状态
• 手动实现:需显式用字段记录状态(如 _index
)
(2) 执行流程控制
• yield
:通过 state
跳转控制执行位置(类似协程)
• 手动实现:直接通过代码顺序控制
(3) 代码生成时机
• yield
:编译时生成代码(查看编译后的DLL可发现额外类)
• 手动实现:开发时直接编写
4. 性能对比
操作 | yield | 手动实现 |
---|---|---|
首次迭代开销 | 稍高(需初始化状态机) | 低 |
后续迭代速度 | 与手动实现相当 | 直接访问字段,略快 |
内存占用 | 每个迭代器实例约 24-32 字节 | 取决于实现方式 |
5. 何时需要手动实现?
尽管 yield
能覆盖 90% 的场景,但在以下情况仍需手动实现迭代器:
- 需要精细控制
Dispose()
逻辑class FileLineReader : IEnumerator<string> {private StreamReader _reader;public void Dispose() {_reader?.Dispose(); // 需要精确释放资源}// 其他实现... }
- 实现非标准迭代行为(如反向迭代、跳跃访问)
class RandomAccessEnumerator : IEnumerator<int> {public bool MoveNext() {_index += Random.Shared.Next(1,3); // 随机跳转return _index < _data.Length;} }
- 需要优化极端性能场景(如游戏引擎核心循环)
6. 通过 ILSpy 反编译验证
使用反编译工具查看 yield
方法的编译结果:
- 原代码:
IEnumerable<int> GetNumbers() {for (int i = 1; i <= 5; i++) {yield return i;} }
- 反编译结果:
▶ 可观察到编译器生成的[CompilerGenerated] private sealed class <GetNumbers>d__0 : IEnumerable<int>, IEnumerator<int> {// 自动生成的状态机字段和方法 }
state
和current
等字段
总结
• 相同点:最终都生成符合迭代器模式的标准实现
• 不同点:
• yield
通过编译器自动生成状态机,大幅简化编码
• 手动实现适合特殊场景需求
• 选择建议:
深入解析 yield
与 List
的本质区别
关键差异分析:
1. 执行时机对比
行为 | yield 版本 | List 版本 |
---|---|---|
代码执行时机 | 每次 MoveNext() 时执行一次循环体 | 在方法返回前立即执行所有循环 |
内存分配时机 | 每次 yield return 时生成当前值 | 方法返回前一次性分配完整集合 |
// yield 版本(按需生成)
IEnumerable<int> GetNumbers() {Console.WriteLine("开始生成");for (int i = 1; i <= 5; i++) {Console.WriteLine($"生成{i}");yield return i; // 每次调用MoveNext()才执行到这里}
}// List 版本(立即生成)
List<int> GetNumbers() {Console.WriteLine("开始生成");var list = new List<int>();for (int i = 1; i <= 5; i++) {Console.WriteLine($"生成{i}");list.Add(i); // 立即执行全部循环}return list;
}// 测试调用
var yieldSeq = GetNumbersYield(); // 此时不输出任何内容
var list = GetNumbersList(); // 立即输出"开始生成"和所有"生成i"
2. 内存分配对比
阶段 | yield 版本 | List 版本 |
---|---|---|
方法调用时 | 不分配具体数字内存 | 立即分配列表内存(含5个元素) |
迭代过程中 | 每次只保留当前数字 | 始终持有完整列表 |
内存占用峰值 | 恒定(仅当前值) | 随元素数量线性增长 |
// 通过内存诊断工具验证
var yieldMemory = GC.GetTotalMemory(false);
var yieldSeq = GetNumbersYield().GetEnumerator();
yieldMemory = GC.GetTotalMemory(false); // 内存几乎不变var listMemory = GC.GetTotalMemory(false);
var list = GetNumbersList();
listMemory = GC.GetTotalMemory(false); // 内存明显增加(含5个int)
3. 底层实现差异
yield
版本(编译器生成状态机)
// 编译器自动生成的类(简化版)
class GeneratedEnumerator : IEnumerator<int> {private int _state;private int _current;private int _i;public bool MoveNext() {switch (_state) {case 0: _i = 1; _state = 1; goto case 1;case 1:if (_i > 5) return false;_current = _i++;_state = 1; // 保持状态return true;}return false;}
}
特点:每次 MoveNext()
只计算当前值,不存储历史数据
List
版本
List<int> list = new List<int>(5); // 预先分配内存
list.Add(1); list.Add(2); /*...*/ list.Add(5);
特点:所有数据已存在于内存中
4. 关键实验验证
通过 延迟执行 证明差异:
// 修改原始方法
IEnumerable<int> GetNumbers() {for (int i = 1; i <= 5; i++) {Thread.Sleep(1000); // 模拟耗时操作yield return i;}
}// 调用代码
var seq = GetNumbers();
Console.WriteLine("开始迭代:" + DateTime.Now);
foreach (var num in seq) { // 每次循环间隔1秒Console.WriteLine(num + " " + DateTime.Now);
}// List版本对比
var list = GetNumbers().ToList(); // 这里会等待5秒
Console.WriteLine("开始迭代:" + DateTime.Now);
foreach (var num in list) { // 立即输出所有数字Console.WriteLine(num + " " + DateTime.Now);
}
输出结果:
yield版本:
开始迭代:12:00:00
1 12:00:01 // 按需生成
2 12:00:02
...List版本:
开始迭代:12:00:05 // 已等待5秒
1 12:00:05 // 立即全部输出
2 12:00:05
...
5. 设计哲学差异
特性 | yield | List |
---|---|---|
设计目标 | 流式处理(Streaming) | 数据快照(Snapshot) |
适用场景 | 大数据/无限序列/实时数据 | 需要随机访问的小数据集 |
资源占用 | 恒定 | 随数据量增长 |
线程安全 | 每次迭代独立 | 需手动同步 |
总结
yield
是 “生成算法”:只在需要时计算当前值(类似实时生产线)List
是 “数据快照”:提前准备好所有结果(类似成品仓库)
C# yield
关键字使用示例详解
yield
是 C# 中用于简化迭代器实现的语法糖,它可以自动生成状态机来管理迭代过程。以下是 6 种典型使用场景的代码示例:
- 基础集合遍历
// 生成1-5的序列
IEnumerable<int> GetNumbers()
{for (int i = 1; i <= 5; i++){yield return i; // 每次迭代返回一个值}// 隐式 yield break
}// 使用
foreach (var num in GetNumbers())
{Console.WriteLine(num); // 输出 1 2 3 4 5
}
- 条件过滤
IEnumerable<int> FilterEven(IEnumerable<int> source)
{foreach (var num in source){if (num % 2 == 0)yield return num; // 只返回偶数}
}// 使用
var numbers = new[] { 1, 2, 3, 4, 5 };
foreach (var even in FilterEven(numbers))
{Console.WriteLine(even); // 输出 2 4
}
- 无限序列
IEnumerable<int> Fibonacci()
{int a = 0, b = 1;while (true){yield return a;(a, b) = (b, a + b); // 元组解构}
}// 使用(需限制次数)
foreach (var num in Fibonacci().Take(10))
{Console.WriteLine(num); // 输出前10个斐波那契数
}
- 状态保持迭代
IEnumerable<string> Tokenize(string input)
{int start = 0;for (int i = 0; i < input.Length; i++){if (char.IsWhiteSpace(input[i])){yield return input.Substring(start, i - start);start = i + 1;}}yield return input[start..]; // 返回最后一个token
}// 使用
foreach (var token in Tokenize("hello world yield"))
{Console.WriteLine(token); // 输出 hello world yield
}
- 组合多个迭代器
IEnumerable<int> Merge(IEnumerable<int> first, IEnumerable<int> second)
{using var e1 = first.GetEnumerator();using var e2 = second.GetEnumerator();while (e1.MoveNext() && e2.MoveNext()){yield return e1.Current;yield return e2.Current;}
}// 使用
var merged = Merge(new[] {1, 3, 5}, new[] {2, 4, 6});
Console.WriteLine(string.Join(",", merged)); // 输出 1,2,3,4,5,6
- 异步流(C# 8.0+)
async IAsyncEnumerable<int> FetchPaginatedData()
{int page = 0;while (true){var data = await GetPageAsync(page++);if (data.Length == 0) yield break;foreach (var item in data)yield return item;}
}// 使用
await foreach (var item in FetchPaginatedData())
{Console.WriteLine(item); // 异步消费数据
}
关键原理说明:
- 延迟执行:调用含
yield
的方法时不会立即执行,直到开始迭代 - 状态机生成:编译器会自动生成实现
IEnumerator
的类 - 内存高效:不需要预先生成整个集合
典型错误示例:
// 错误!yield不能在try-catch中带catch块
IEnumerable<int> BadExample()
{try {yield return 1; // ❌ 编译错误}catch { /* 不允许 */ }
}// 正确写法:把try放在yield外部
IEnumerable<int> GoodExample()
{try {foreach (var item in GetItems())yield return item;}catch { /* 处理异常 */ }
}
性能对比:
方法 | 内存消耗 | 启动延迟 | 适用场景 |
---|---|---|---|
yield | 低 | 低 | 大数据集/流式处理 |
List<T> | 高 | 高 | 需要多次访问的小数据集 |
Array | 中 | 低 | 固定大小的数据集 |
最佳实践:当处理未知大小的数据流时,优先使用
yield
可以避免内存爆炸问题。
实际项目中 yield
的典型应用场景与代码示例
yield
在真实项目开发中能显著提升代码的内存效率和可维护性。以下是 6 个实际案例,附带完整代码和场景说明:
- 分页查询数据库(避免内存爆炸)
// 数据库访问层
public IEnumerable<User> GetUsersBatch(int batchSize = 1000)
{int page = 0;while (true) {var users = _dbContext.Users.OrderBy(u => u.Id).Skip(page * batchSize).Take(batchSize).ToList();if (users.Count == 0) yield break;foreach (var user in users) {yield return user; // 流式返回}page++;}
}// 调用方(处理100万用户仅需恒定内存)
foreach (var user in GetUsersBatch())
{ProcessUser(user); // 每次只加载batchSize条数据
}
优势:避免一次性加载全部数据导致OOM
- 动态生成日志流水号
public static IEnumerable<string> GenerateSerialNumbers(string prefix)
{int counter = 1;while (true) {yield return $"{prefix}-{DateTime.Now:yyyyMMdd}-{counter++:D5}";// 示例输出: "LOG-20240523-00001"}
}// 使用案例
var logNumbers = GenerateSerialNumbers("LOG");
var currentNumber = logNumbers.Take(1).First(); // 获取下一个可用编号
- 递归遍历文件夹(惰性加载)
public static IEnumerable<FileInfo> FindFiles(string path, string searchPattern)
{foreach (var file in Directory.EnumerateFiles(path, searchPattern)){yield return new FileInfo(file);}foreach (var dir in Directory.EnumerateDirectories(path)) {foreach (var file in FindFiles(dir, searchPattern)) // 递归{yield return file;}}
}// 使用案例(即时处理TB级存储)
foreach (var file in FindFiles(@"D:\Data", "*.csv"))
{ProcessLargeFile(file); // 每次只处理一个文件
}
- 游戏技能冷却计时器
public IEnumerable<float> SkillCooldown(float duration)
{float remaining = duration;while (remaining > 0) {yield return remaining;remaining -= Time.deltaTime; // Unity引擎帧时间}yield return 0;
}// Unity协程使用
IEnumerator UseSkill()
{foreach (var timeLeft in SkillCooldown(5f)) {UpdateUI(timeLeft); // 更新冷却UIyield return null; // 等待下一帧}EnableSkill(); // 冷却结束
}
- API限流请求批处理
public IEnumerable<List<T>> BatchRequests<T>(IEnumerable<T> source, int batchSize)
{var batch = new List<T>(batchSize);foreach (var item in source) {batch.Add(item);if (batch.Count >= batchSize) {yield return batch;batch = new List<T>(batchSize);}}if (batch.Count > 0) yield return batch;
}// 调用第三方API(限制每秒100条)
var data = GetHugeDataFromDB(); // 假设100万条
foreach (var batch in BatchRequests(data, 100))
{await CallExternalApi(batch); // 分批调用await Task.Delay(1000); // 限速
}
- 实时股票价格推送
public IEnumerable<StockPrice> SubscribePrices(string symbol)
{var random = new Random();while (true) {var price = new StockPrice(symbol,Math.Round(100 + random.NextDouble() * 10, 2),DateTime.Now);yield return price;Thread.Sleep(2000); // 每2秒推送}
}// WebSocket 推送示例
[HttpGet("prices/{symbol}")]
public async Task PriceStream(string symbol)
{Response.ContentType = "text/event-stream";foreach (var price in SubscribePrices(symbol)) {await Response.WriteAsync($"data: {price}\n\n");await Response.Body.FlushAsync();}
}
关键设计原则
- 惰性计算:只在调用
MoveNext()
时执行代码 - 资源释放:用
using
包裹需要释放的资源 - 异常处理:避免在
yield return
的try
块中放catch
- 线程安全:默认非线程安全,需要时加锁
性能对比测试
处理100万条数据时的内存占用:
方法 | 内存峰值 | GC 压力 |
---|---|---|
yield | 2MB | Gen0 |
List<T> | 200MB | Gen2 |
何时应该避免使用?
• 需要随机访问元素时(如 list[999]
)
• 需要多次遍历同一数据集时(应 .ToList()
缓存)
• 方法有副作用(如修改全局状态)