直接写 IEnumerator 容易出错,因需手动管理状态机、线程安全和边界判断;应使用 yield return 让编译器自动生成状态机类,返回 IEnumerable 或 IEnumerator,注意方法签名与变量生命周期。

为什么直接写 IEnumerator 容易出错
很多人一上来就手动实现 IEnumerator 接口,结果卡在 MoveNext() 状态维护、Reset() 报 NotSupportedException、或 Current 返回类型不匹配上。根本原因是:手写迭代器要自己管理状态机、线程安全、边界判断,稍有疏漏就会抛 InvalidOperationException 或返回错误值。
真正该走的路是让编译器帮你生成——用 yield return,它背后会自动构造一个私有嵌套类(继承 IEnumerator<t></t> 和 IDisposable),把所有状态逻辑包圆了。
-
yield return方法必须返回IEnumerable<t></t>或IEnumerator<t></t>,不能是void - 方法体内不能有
return语句(除return;外),也不能有ref/out参数 - 局部变量会在状态机中被提升为字段,所以闭包捕获要小心生命周期(比如循环变量 i 被反复覆盖)
怎么给自定义集合类添加 IEnumerable<t></t> 支持
不是所有类都天然支持 foreach。如果你写了个 BookCollection 类,想让它能被遍历,就得显式实现 IEnumerable<t></t> 接口——但别重写整个接口,只实现 GetEnumerator() 方法即可,其余由编译器补全。
最简实践是:在类里加一个公开的 GetEnumerator() 方法,返回类型为 IEnumerator<t></t>,内部用 yield return 遍历内部数据结构。
public class BookCollection : IEnumerable<string>
{
private readonly List<string> _books = new() { "C# in Depth", "CLR via C#" };
<pre class="brush:php;toolbar:false;">public IEnumerator<string> GetEnumerator() => _books.GetEnumerator();
// 或更灵活的自定义逻辑:
// public IEnumerator<string> GetEnumerator()
// {
// foreach (var book in _books)
// yield return book.ToUpperInvariant();
// }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();}
- 必须同时提供泛型和非泛型两个
GetEnumerator()方法,否则foreach会报错 - 非泛型版本只需调用泛型版,不要重复逻辑
- 如果内部存储不是
List<t></t>,比如是链表或字典,就别直接转发GetEnumerator(),改用yield return手动控制输出顺序
yield break 在什么场景下必须用
yield break 不是可有可无的语法糖,它是提前终止迭代的唯一干净方式。比如过滤空值、按条件截断、或处理分页数据时,不用它就只能靠外层 if 套 yield return,代码臃肿且易漏逻辑。
常见误用是把它当成 return 来退出方法——它只退出迭代过程,不结束方法执行;后续代码仍可能运行(除非它在最后)。
- 空集合或条件不满足时,直接
yield break,避免返回null或抛异常 - 分页场景:
if (index >= pageSize) yield break;比在每轮循环里加if更清晰 - 不要在
finally块里用yield break—— 编译器不允许多个控制流出口
性能和兼容性要注意的三个点
yield return 是延迟执行的,每次 MoveNext() 才跑对应那轮逻辑。这带来便利,也埋了坑:多次遍历同一 IEnumerable<t></t> 可能重复计算;内部状态不可重入;.NET Framework 和 .NET 5+ 对泛型协变的支持也有差异。
- 如果数据源是数据库查询或耗时 IO,考虑用
.ToList()缓存结果,避免重复执行 -
IEnumerable<t></t>不保证线程安全,多线程并发调用GetEnumerator()会出问题,需自行加锁或改用ImmutableArray<t></t> - 在 .NET Standard 2.0 或旧项目中,确保目标框架支持迭代器(.NET Framework 2.0+ 都支持,但某些 Unity 版本默认禁用)
真正难的不是写出能跑的迭代器,而是想清楚:这个遍历要不要缓存、要不要支持多次枚举、要不要暴露原始数据结构细节。这些决策比语法本身影响更大。


















