C#中实现迭代器应优先使用yield return声明,由编译器自动生成状态机;手写IEnumerator易错难维护,仅在需精细控制资源或兼容旧框架时采用。

直接说结论:C#里实现迭代器模式,优先用 yield return,而不是手写 IEnumerable<t></t> 和 IEnumerator<t></t>;手写只在需要精细控制状态、资源或兼容旧框架时才考虑。
为什么 foreach 能遍历一个类,它甚至没实现 IEnumerable<T>
因为 foreach 不查接口,只做鸭子类型检查:只要这个类型有公开的 GetEnumerator() 方法,且返回值类型有 MoveNext() 和 Current 成员(不管是不是 IEnumerator<t></t>),就能跑。
- 这意味着你可以给一个普通 class 加个方法:
public MyCustomEnumerator GetEnumerator() { ... },然后直接foreach (var x in instance) -
yield return生成的匿名类,内部既没显式实现IEnumerable<t></t>,也没暴露IEnumerator<t></t>类型名,但照样能被foreach消费 - 如果你手动实现
GetEnumerator()返回IEnumerator(非泛型),foreach也认——只是会丢失类型安全
yield return 的实际行为和常见陷阱
yield return 看似简单,背后是编译器生成的状态机,不是普通方法调用。它的执行时机、生命周期和异常表现都和直觉不同。
- 每次调用
yield方法(如GetNumbers())都会新建一个迭代器实例,彼此状态隔离;不会“接着上次停的地方继续” -
Current在MoveNext()返回false后访问,必须抛InvalidOperationException,否则foreach会静默失败或重复取最后值 - 在
yield方法中捕获局部变量(比如var now = DateTime.Now),该变量会被提升为生成类的字段,延长生命周期——可能造成意外内存驻留 - 不能把
yield return放在try-catch块里,但可以放在try-finally的try中;yield break不能放finally里
手写 IEnumerator<T> 时最容易漏掉的三件事
手动实现比 yield 更底层,也更容易出错。不是光写 MoveNext() 和 Current 就完事。
-
Dispose()必须幂等:可被多次调用且不抛异常;如果封装了FileStream或数据库连接,这是唯一可靠的释放点 -
Reset()不建议留空;.NET Core+ 允许不实现,但若实现,必须明确抛NotSupportedException,否则某些 LINQ 操作(如ToList())可能误用它 -
Current属性在迭代未开始(MoveNext()没调过)或已结束(MoveNext()返回false)时访问,必须抛InvalidOperationException,不是NullReferenceException
真正难的不是写出能跑的迭代器,而是写出符合 .NET 迭代协议、能和 LINQ、foreach、ToArray() 等所有消费者稳定协作的迭代器。尤其要注意 Dispose() 的幂等性和 Current 的非法访问边界——这两点在线上环境出问题时,往往表现为偶发性数据错乱或资源泄漏,极难复现。


















