欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > c# 深度剖析yield关键字

c# 深度剖析yield关键字

2025/4/28 23:03:18 来源:https://blog.csdn.net/weixin_44291381/article/details/147511256  浏览:    关键词:c# 深度剖析yield关键字

文章目录

        • `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% 的场景,但在以下情况仍需手动实现迭代器:

  1. 需要精细控制 Dispose() 逻辑
    class FileLineReader : IEnumerator<string> {private StreamReader _reader;public void Dispose() {_reader?.Dispose(); // 需要精确释放资源}// 其他实现...
    }
    
  2. 实现非标准迭代行为(如反向迭代、跳跃访问)
    class RandomAccessEnumerator : IEnumerator<int> {public bool MoveNext() {_index += Random.Shared.Next(1,3); // 随机跳转return _index < _data.Length;}
    }
    
  3. 需要优化极端性能场景(如游戏引擎核心循环)

6. 通过 ILSpy 反编译验证
使用反编译工具查看 yield 方法的编译结果:

  1. 原代码:
    IEnumerable<int> GetNumbers() {for (int i = 1; i <= 5; i++) {yield return i;}
    }
    
  2. 反编译结果:
    [CompilerGenerated]
    private sealed class <GetNumbers>d__0 : IEnumerable<int>, IEnumerator<int> {// 自动生成的状态机字段和方法
    }
    
    ▶ 可观察到编译器生成的 statecurrent 等字段

总结
• 相同点:最终都生成符合迭代器模式的标准实现

• 不同点:

yield 通过编译器自动生成状态机,大幅简化编码

• 手动实现适合特殊场景需求

• 选择建议:

普通需求
特殊需求
需要迭代功能
使用 yield
手动实现
深入解析 yieldList 的本质区别

关键差异分析:


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. 设计哲学差异

特性yieldList
设计目标流式处理(Streaming)数据快照(Snapshot)
适用场景大数据/无限序列/实时数据需要随机访问的小数据集
资源占用恒定随数据量增长
线程安全每次迭代独立需手动同步

总结

  1. yield 是 “生成算法”:只在需要时计算当前值(类似实时生产线)
  2. List 是 “数据快照”:提前准备好所有结果(类似成品仓库)
C# yield 关键字使用示例详解

yield 是 C# 中用于简化迭代器实现的语法糖,它可以自动生成状态机来管理迭代过程。以下是 6 种典型使用场景的代码示例:

  1. 基础集合遍历
// 生成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
}
  1. 条件过滤
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
}
  1. 无限序列
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个斐波那契数
}
  1. 状态保持迭代
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
}
  1. 组合多个迭代器
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
  1. 异步流(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); // 异步消费数据
}

关键原理说明:

  1. 延迟执行:调用含yield的方法时不会立即执行,直到开始迭代
  2. 状态机生成:编译器会自动生成实现IEnumerator的类
  3. 内存高效:不需要预先生成整个集合

典型错误示例:

// 错误!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 个实际案例,附带完整代码和场景说明:


  1. 分页查询数据库(避免内存爆炸)
// 数据库访问层
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


  1. 动态生成日志流水号
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(); // 获取下一个可用编号

  1. 递归遍历文件夹(惰性加载)
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); // 每次只处理一个文件
}

  1. 游戏技能冷却计时器
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(); // 冷却结束
}

  1. 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);       // 限速
}

  1. 实时股票价格推送
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();}
}

关键设计原则

  1. 惰性计算:只在调用 MoveNext() 时执行代码
  2. 资源释放:用 using 包裹需要释放的资源
  3. 异常处理:避免在 yield returntry 块中放 catch
  4. 线程安全:默认非线程安全,需要时加锁

性能对比测试
处理100万条数据时的内存占用:

方法内存峰值GC 压力
yield2MBGen0
List<T>200MBGen2

何时应该避免使用?
• 需要随机访问元素时(如 list[999]

• 需要多次遍历同一数据集时(应 .ToList() 缓存)

• 方法有副作用(如修改全局状态)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词