为什么 IEnumerator<T> 继承自 IDisposable,而非泛型之外的 IEnumerator 不继承?

86
我注意到通用的IEnumerator<T>继承自IDisposable,但非通用接口IEnumerator没有。为什么会这样设计呢?
通常,我们使用foreach语句遍历IEnumerator<T>实例。foreach生成的代码实际上有try-finally块,在finally中调用Dispose()方法。
6个回答

94
基本上这是一个疏忽。在C# 1.0中,foreach从未调用Dispose1。随着C# 1.2(引入于VS2003 - 奇怪的是没有1.1),foreach开始在finally块中检查迭代器是否实现了IDisposable - 他们不得不这样做,因为对IEnumerator进行回顾性的扩展会破坏每个人的IEnumerator实现。如果他们当初意识到让foreach来处理迭代器释放资源很有用,我相信IEnumerator就会扩展IDisposable

然而,当C# 2.0和.NET 2.0推出时,他们有了新的机会 - 新的接口,新的继承。将接口扩展IDisposable更加合理,这样你就不需要在finally块中进行执行时检查,并且现在编译器知道如果迭代器是IEnumerator<T>,它可以发出无条件调用Dispose

编辑:在迭代结束时调用Dispose非常有用(无论如何它都会结束)。这意味着迭代器可以保持对资源的控制 - 这使得它可以逐行读取文件,例如。迭代器块会生成Dispose实现,确保与迭代器的“当前执行点”相关联的任何finally块在释放时都会执行 - 因此您可以在迭代器内编写普通代码,并且清理工作应该会恰当地进行。


1 回顾1.0规范,已经指定了。我还没有能够验证这个早期说法,即1.0实现没有调用Dispose


我需要期望一个非泛型的 IEnumerable.GetEnumerator 也是 IDisposable 吗? - Shimmy Weitzhandler
2
@Shimmy:接受非通用 IEnumerable 任意实现的代码有责任确保从 GetEnumerator 返回的任何可处理对象都将被处理。不这样做的代码应被视为不完整。 - supercat
@supercat:自从写下这个答案以来,我发现它已经在1.0规范中了。我还没有找到一个1.0版本的安装包来检查我的行为是否正确。我会进行编辑。 - Jon Skeet
2
@JonSkeet:如果 C# 的任何版本在 foreach 中没有 Dispose 逻辑,则其与 VisualBasic.Collection 类(该类在许多方面都很烦人和古怪,但与当时的其他 Microsoft 集合不同,允许在枚举期间删除项)会表现得非常糟糕。Collection 类避免保留对未完成枚举器的任何强引用,并将在它们被垃圾回收时清理它们,但是如果集合在 GC 循环之间被枚举多次并且这些枚举器没有被清理,则会变得非常缓慢。 - supercat
@supercat:当然这并不意味着它没有发生... ;) 这就是为什么我想要验证它。 - Jon Skeet

6

IEnumerable<T>不继承IDisposable接口。但是,IEnumerator<T>继承了IDisposable接口,而非泛型的IEnumerator没有。即使您使用foreach循环遍历非泛型的IEnumerable(它返回IEnumerator),编译器仍会生成IDisposable检查,并在枚举器实现该接口时调用Dispose()方法。

我猜想,泛型的Enumerator<T>继承了IDisposable接口,因此不需要运行时类型检查-它可以直接调用Dispose()方法,这应该具有更好的性能,因为如果枚举器具有空的Dispose()方法,则可以进行优化。


如果编译器在编译时知道 IEnumerable,以便它可以优化调用 Dispose 的过程,那么它也可以优化类型检查。 - Edward Brey

4

我最近编写了一个库,在其中使用了 IEnumerable of T / IEnumerator of T,用户可以实现自定义迭代器,只需实现 IEnumerator of T 即可。

我发现 IEnumerator of T 继承了 IDisposable,这让我觉得非常奇怪。我们只有想要释放非托管资源时才会实现 IDisposable,对吧?那么只有实际持有非托管资源的枚举器(如 IO 流等)才相关。如果有意义,为什么不让用户在其枚举器上同时实现 IEnumerator of T 和 IDisposable 呢?在我看来,这违反了单一责任原则 - 为什么要混淆枚举逻辑和对象销毁呢。


3
如果 GetEnumerator 返回需要清理的对象(例如因为它正在从文件中读取数据行,这些数据行必须关闭),则了解枚举器何时不再需要的实体必须有某种方式将该信息传达给能够执行清理的实体。IDisposable 在Liskov替换原则方面表现出相反的行为,因为返回可能需要清理的内容的工厂不能安全地替换承诺返回不需要清理的内容的工厂,但是相反的替换是安全的,应该是合法的。 - supercat
我也觉得在IEnumerator<T>上使用IDisposable有点困惑,查看一下.NET源代码中List<T>Enumerator如何实现IDisposable会很有帮助。请注意,Enumerator结构体具有一个Dispose方法,但其中没有任何内容。请注意,这种IDisposable行为绝不意味着“List<Bitmap>应该在foreach中释放其Bitmap!” - jrh
相关:IEnumerator:Dispose方法为空是否正常?(答案:是的,这是正常的) - jrh
2
@supercat 在修正接口隔离原则破坏方面很好地使用了 Liskov 替换原则 ;) - mireazma
@mireazma:如果容器类型与对象实例类型有稍微不同的层次结构,将会很有帮助。其中,容器可以根据它们拥有独占引用、拥有公开引用、引用由其他人拥有的对象或引用无所有者的对象进行分类。集合应该如何实现诸如相等性检查之类的方法应取决于这些区别,但语言无法传达它们。 - supercat

0
IEnumerable` 继承自 IDisposing 吗?根据 .NET 反射器或 MSDN。你确定你不是和 IEnumerator 混淆了吗?IEnumerator 使用 IDisposing,因为它仅用于枚举集合,而不是长期使用。

IDisposing?你是指“不是根据…”吗? - Peter Ritchie
3
我给你点了一个+1来抵消-1,因为您的留言是在原帖作者错误地指定IEnumerable和IEnumerator之间的时间发布的,很可能促使原帖作者修复问题。不过,由于这个问题早已被解决,所以您可以删除自己的“答案”,因为它已经没有任何用处了。 - supercat
@supercat ... 还有差不多10年过去了... XD - spamove

0

除非你能得到AndersH本人或他身边的人的回应,否则很难确定这个问题。

然而,我的猜测是它与在C#中引入的“yield”关键字有关。如果你查看编译器在使用“yield return x”时生成的代码,你会看到该方法被包装在一个实现IEnumerator的帮助类中;让IEnumerator从IDisposable继承确保它可以在枚举完成时进行清理。


yield 只会让编译器生成状态机的代码,这些代码不需要超出正常垃圾回收范围以外的任何处理。 - Mark Cidade
@marxidad:完全不正确。考虑一下如果“using”语句出现在迭代器块中会发生什么。请参见http://csharpindepth.com/Articles/Chapter6/IteratorBlockImplementation.aspx。 - Jon Skeet
@Jon:并非完全不正确。尽管 IDisposable 不是对于不使用 using 的情况严格必要的,但只需将所有新样式枚举器设为可处理即可。这样每次都调用 Dispose() 更为简单,以防万一。 - Mark Cidade
我本来想修改我的评论,让它变得比“完全不正确”温和一些,但是声称编译器生成的代码不需要任何处理仍然是不准确的。在我看来,它有时候确实需要处理,而你的笼统陈述是不正确的。 - Jon Skeet
如果创建并且未被处理的枚举器过多,VisualBasic.Collection 类的性能将会非常低(比正常情况下慢几个数量级)。因此,在发布 .net 1.0 之前就已经明显需要将 DisposeIEnumerable 关联起来,但可能太晚了,将 IEnumerable 继承到 IDisposable 可能会影响发布计划。 - supercat

-2

如果我没记错,关于有 IEnumerable<T>IEnumerable 的整件事情是由于 IEnumerable 比 .Net 的模板还要早。我怀疑你的问题也是同样的道理。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接