为什么IEnumerator <T>从IDisposable继承而非通用IEnumerator不继承?

Mor*_*eng 72 c# generics ienumerator

我注意到泛型IEnumerator<T>继承自IDisposable,但非通用接口IEnumerator没有.为什么这样设计?

通常,我们使用foreach语句来遍历IEnumerator<T>实例.生成的foreach代码实际上有try-finally块,最终调用Dispose().

Jon*_*eet 80

基本上这是一个疏忽.在C#1.0中,foreach 从未调用过Dispose 1.随着C#1.2(在VS2003中引入 - 没有1.1,奇怪)foreach开始在finally块中检查迭代器是否实现IDisposable- 他们必须这样做,因为追溯性地进行IEnumerator扩展IDisposable会破坏每个人的实现IEnumerator.如果他们认为首先foreach处理迭代器是有用的,我肯定IEnumerator会扩展IDisposable.

然而,当C#2.0和.NET 2.0问世时,他们有了一个新的机会 - 新的界面,新的继承.让接口扩展IDisposable以便在finally块中不需要执行时检查更有意义,现在编译器知道如果迭代器是一个IEnumerator<T>它可以发出无条件调用Dispose.

编辑:Dispose在迭代结束时被调用是非常有用的(但它结束了).这意味着迭代器可以保留资源 - 这使得它可以逐行读取文件.迭代器阻塞生成器Dispose实现,确保finally与迭代器的"当前执行点"相关的任何块在处理时执行 - 因此您可以在迭代器中编写正常代码并进行适当的清理.


1回顾1.0规范,已经指定了.我还没有能够验证这个1.0实现没有调用的早期声明Dispose.

  • @Shimmy:接受非通用“IEnumerable”任意实现的代码有义务确保从“GetEnumerator”返回的任何一次性对象都将被处置。不应该被视为损坏的代码。 (3认同)
  • @JonSkeet:任何在“foreach”中没有“Dispose”逻辑的 C# 版本在“VisualBasic.Collection”类中的表现都会很差(这在很多方面都很烦人和古怪,但是——与任何其他 Microsoft 不同)那个时代的集合——允许在枚举期间删除项目)。“Collection”类避免持有对未完成枚举器的任何强引用,并且如果它们被垃圾收集,则会清理它们,但如果在 GC 周期之间多次枚举集合并且这些枚举器没有被清理,它将得到*非常慢。 (2认同)

Mar*_*ade 5

IEnumerable <T>不继承IDisposable.IEnumerator <T>确实继承了IDisposable,而非通用IEnumerator却没有.即使将foreach用于非泛型IEnumerable(返回IEnumerator),编译器仍将生成对IDisposable的检查,并在枚举器实现接口时调用Dispose().

我猜通用的Enumerator <T>继承自IDisposable,所以不需要运行时类型检查 - 它可以继续调用Dispose(),它应该具有更好的性能,因为它可能会被优化掉,如果枚举器有一个空的Dispose()方法.


小智 5

我合理地编写了一个我使用的库IEnumerable of T/IEnumerator of T库的用户可以在其中实现他们应该实现的自定义迭代器IEnumerator of T

我发现 T 的 IEnumerator 会从 IDisposable 继承非常奇怪。如果我们想释放非托管资源,我们实施 IDisposable 对吗?所以它只与实际持有非托管资源的枚举器相关 - 如 IO 流等。如果有意义,为什么不让用户在他们的枚举器上实现 T 的 IEnumerator 和 IDisposable 呢?在我的书中,这违反了单一职责原则 - 为什么混合枚举器逻辑和处理对象。

  • 如果`GetEnumerator` 返回一个需要清理的对象(例如,因为它正在从必须关闭的文件中读取数据行),那么知道何时不再需要枚举器的实体必须有某种方式将该信息传达给某些可以执行清理的实体。`IDisposable` 在 Liskov 替换原则方面表现落后,因为返回可能需要清理的东西的工厂不能安全地替代承诺返回不需要清理的东西的工厂,但反向替换是安全的,并且*应该*合法。 (2认同)
  • @supercat 很好地利用了里氏替换原则来修正对接口隔离原则的破坏;) (2认同)