LINQ 表达式中的多次枚举
发布: (2025年12月15日 GMT+8 14:00)
4 min read
原文: Dev.to
Source: Dev.to
为什么多次枚举是个问题
- 重复工作 – 每次枚举都会创建一个新的枚举器,从序列的开头重新开始。
- 资源消耗 – 对于查询数据库、调用 Web 服务或读取文件的情况,每次枚举都会重复执行代价高的操作。
- 意外的副作用 – 某些枚举器可能具有有状态的副作用(例如日志记录、计数器),这些副作用会被多次触发。
及早物化
为了避免重复枚举,在知道需要遍历多次时就将序列物化。物化会把惰性 IEnumerable 转换为具体的集合(List、T[] 等),从而可以重复使用而无需重新执行底层查询。
// Lazy sequence
IEnumerable numbers = Enumerable.Range(1, 10);
// This will enumerate the sequence twice
foreach (int n in numbers) Console.WriteLine(n);
foreach (int n in numbers) Console.WriteLine(n);
使用 ToList()(或 ToArray())进行物化
IEnumerable numbers = Enumerable.Range(1, 10);
List materializedNumbers = numbers.ToList(); // materialized once
// Both loops now use the same in‑memory list
foreach (int n in materializedNumbers) Console.WriteLine(n);
foreach (int n in materializedNumbers) Console.WriteLine(n);
最佳实践
| 建议 | 原因 |
|---|---|
| 及早物化 当你知道集合会被多次迭代时。 | 确保原始源只枚举一次。 |
使用 ToList() 或 ToArray() 将 IEnumerable 转换为具体类型。 | 提供快速的索引访问并避免重复执行查询。 |
| 为会被多次枚举的方法参数添加文档说明。 | 提醒调用者潜在的性能成本。 |
对代码进行性能分析,定位同一 IEnumerable 被重复枚举的热点。 | 帮助你将优化工作集中在最关键的地方。 |
| 理解延迟执行(参见第 1 部分),以预测何时会发生枚举。 | 防止意外对惰性查询进行多次遍历。 |
多次枚举特别有害的场景
- 数据库查询 – 每次枚举可能重新执行 SQL 命令,导致多次往返。
- 外部 API 调用 – 重新枚举会触发重复的网络请求。
- 文件 I/O – 多次读取同一文件会浪费 I/O 带宽。
- 昂贵的计算 – 重新计算复杂转换会降低性能。
快速检查清单
- 我是否需要对集合进行多次迭代?
- 如果需要,是否已经调用一次
ToList()/ToArray()并复用结果? - 是否已经为会被多次枚举的参数添加了文档说明?
- 是否已经对代码进行分析,确保没有隐藏的多次枚举?
进一步阅读
- 第 1 部分: Deferred Execution – The Essence of LINQ in C#
- 即将发布: LINQ Performance Optimization: 5 Patterns Every C# Developer Should Know