85

I noticed that the generic IEnumerator<T> inherits from IDisposable, but the non-generic interface IEnumerator does not. Why is it designed in this way?

Usually, we use foreach statement to go through a IEnumerator<T> instance. The generated code of foreach actually has try-finally block that invokes Dispose() in finally.

sashoalm
  • 75,001
  • 122
  • 434
  • 781
Morgan Cheng
  • 73,950
  • 66
  • 171
  • 230

6 Answers6

93

Basically it was an oversight. In C# 1.0, foreach never called Dispose 1. With C# 1.2 (introduced in VS2003 - there's no 1.1, bizarrely) foreach began to check in the finally block whether or not the iterator implemented IDisposable - they had to do it that way, because retrospectively making IEnumerator extend IDisposable would have broken everyone's implementation of IEnumerator. If they'd worked out that it's useful for foreach to dispose of iterators in the first place, I'm sure IEnumerator would have extended IDisposable.

When C# 2.0 and .NET 2.0 came out, however, they had a fresh opportunity - new interface, new inheritance. It makes much more sense to have the interface extend IDisposable so that you don't need an execution-time check in the finally block, and now the compiler knows that if the iterator is an IEnumerator<T> it can emit an unconditional call to Dispose.

EDIT: It's incredibly useful for Dispose to be called at the end of iteration (however it ends). It means the iterator can hold on to resources - which makes it feasible for it to, say, read a file line by line. Iterator blocks generate Dispose implementations which make sure that any finally blocks relevant to the "current point of execution" of the iterator are executed when it's disposed - so you can write normal code within the iterator and clean-up should happen appropriately.


1 Looking back at the 1.0 spec, it was already specified. I haven't yet been able to verify this earlier statement that the 1.0 implementation didn't call Dispose.

Edward Brey
  • 40,302
  • 20
  • 199
  • 253
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • do i have to expect an `IEnumerable.GetEnumerator` (non-generic) to be `IDisposable` as well? – Shimmy Weitzhandler Jan 08 '12 at 09:20
  • 2
    @Shimmy: Code which accepts arbitrary implementations of non-generic `IEnumerable` is obligated to ensure that any disposable object returned from `GetEnumerator` will be disposed. Code which does not should be considered broken. – supercat Jan 28 '13 at 18:00
  • @supercat: Since writing this answer, I've found that it was already in the 1.0 spec. I haven't managed to get hold of a 1.0 installation to check whether I was actually right or not about the behaviour. Will edit. – Jon Skeet Jan 28 '13 at 18:13
  • 2
    @JonSkeet: Any version of C# which didn't have `Dispose` logic in `foreach` would have behaved very poorly with the `VisualBasic.Collection` class (which is annoying and quirky in many ways, but--unlike any other Microsoft collections of that era--allows items to be removed during enumeration). The `Collection` class avoids holding any strong references to outstanding enumerators, and will clean them up if they get garbage-collected, but if a collection is enumerated many times between GC cycles and those enumerators aren't cleaned up, it will get *very* slow. – supercat Jan 28 '13 at 18:19
  • @supercat: That doesn't mean it didn't happen, of course... ;) That's why I want to verify it. – Jon Skeet Jan 28 '13 at 18:25
6

IEnumerable<T> doesn't inherit IDisposable. IEnumerator<T> does inherit IDisposable however, whereas the non-generic IEnumerator doesn't. Even when you use foreach for a non-generic IEnumerable (which returns IEnumerator), the compiler will still generate a check for IDisposable and call Dispose() if the enumerator implements the interface.

I guess the generic Enumerator<T> inherits from IDisposable so there doesn't need to be a runtime type-check—it can just go ahead and call Dispose() which should have better performance since it can be probably be optimized away if the enumerator has an empty Dispose() method.

Mark Cidade
  • 98,437
  • 31
  • 224
  • 236
  • If the compiler knows the `IEnumerable` at compile so, such that it can optimize away the call to `Dispose`, it can also optimize away a type check. – Edward Brey Apr 09 '19 at 11:31
4

I reasontly wrote a library where I used IEnumerable of T / IEnumerator of T where users of the library could implement custom iterators they should just implement IEnumerator of T.

I found it very strange that IEnumerator of T would inherit from IDisposable. We implement IDisposable if we want to free unmanaged resources right? So it would only be relevant for enumerators that actually hold unmanaged resources - like an IO stream etc. Why not just let users implement both IEnumerator of T and IDisposable on their enumerator if it makes sense? In my book this violates the single responsibility principle - Why mix enumerator logic and disposing objects.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
JAXN
  • 49
  • 1
  • 1
  • 3
    If `GetEnumerator` returns an object that requires cleanup (e.g. because it's reading lines of data from a file which will have to be closed), an entity that knows when the enumerator is no longer needed must have some means of conveying that information to some entity that can perform the cleanup. `IDisposable` behaves backward with regard to the Liskov Substitution Principle, since a factory which returns things that may require cleanup is not safely substitutable for one that promises to return things that don't, but the reverse substitution would be safe and *should* be legitimate. – supercat Jan 28 '13 at 17:57
  • I also found `IDisposable` on `IEnumerator` to be a little confusing, I found it helpful to check out how the [`Enumerator` of `List` implemented `IDisposable`](https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,1154) in the .NET source. Note how the `Enumerator` `struct` has a `Dispose` method but there's nothing in it. **Note that this `IDisposable` behavior in no way implies something like "A `List` should dispose its `Bitmap`s in `foreach`!**" – jrh Jan 26 '17 at 16:21
  • Related: [IEnumerator: Is it normal to have an empty Dispose method?](http://stackoverflow.com/questions/3061612/ienumerator-is-it-normal-to-have-an-empty-dispose-method) (answer: yes, it is) – jrh Jan 26 '17 at 18:04
  • 2
    @supercat good use of Liskov Substitution Principle for amending the breaking of Interface Segregation Principle ;) – mireazma Sep 26 '20 at 11:02
  • @mireazma: It would be helpful if container types had a slightly different hierarchy from object-instance types, with among other things containers being classifiable according to whether they own exclusive references, own exposed references, refer to objects owned by others, or refer to ownerless objects. The way a collection should implement methods for things like equality checking should depend upon such distinctions, but there's no way for the language to convey them. – supercat Sep 26 '20 at 12:11
0

Does IEnumerable` inherit IDisposing? According to the .NET reflector or MSDN. Are you sure you're not confusing it with IEnumerator? That uses IDisposing because it only for enumerating a collection and not meant for longevity.

Aaron Powell
  • 24,927
  • 18
  • 98
  • 150
  • IDisposing? Also, do you mean "Not according to..."? – Peter Ritchie Nov 08 '11 at 15:08
  • 3
    I gave you a +1 to counteract the -1, since your message was posted between the time the original poster incorrectly specified IEnumerable, and IEnumerator and was likely what prompted the OP to fix his question. Nonetheless, since the question has long since been fixed, you may as well delete your "answer" since it is certainly no longer applicable. – supercat Feb 02 '13 at 20:21
  • @supercat ... aaand almost 10 years later... XD – spamove Sep 02 '22 at 19:35
0

A bit hard to be definitive on this, unless you manage to get a response from AndersH himself, or someone close to him.

However, my guess is that it relates to the "yield" keyword that was introduced in C# at the same time. If you look at the code generated by the compiler when "yield return x" is used, you'll see the method wrapped up in a helper class that implements IEnumerator; having IEnumerator descend from IDisposable ensures that it can clean up when enumeration is complete.

Mark Cidade
  • 98,437
  • 31
  • 224
  • 236
Bevan
  • 43,618
  • 10
  • 81
  • 133
  • yield just makes the compiler generate code for a state machine which doesn't need any disposing beyond normal GC – Mark Cidade Oct 24 '08 at 06:02
  • @marxidad: Entirely incorrect. Consider what happens if a "using" statement occurs in an iterator block. See http://csharpindepth.com/Articles/Chapter6/IteratorBlockImplementation.aspx – Jon Skeet Oct 24 '08 at 06:15
  • @Jon: Not entirely incorrect. Although IDisposable isn't strictly necessary for cases where using *isn't* used, it is simpler to just make all new-style enumerators disposable and call Dispose() every time just in case. – Mark Cidade Oct 24 '08 at 07:02
  • I was about to reword my comment to make it a little bit softer than "entirel incorrect" but claiming that the compiler generates code which doesn't need any disposing is still inaccurate IMO. It *sometimes* does, but your blanket statement was incorrect. – Jon Skeet Oct 24 '08 at 07:10
  • The `VisualBasic.Collection` class will perform poorly (several orders of magnitude slower than normal) if many enumerators are created and abandoned without being disposed. The need to associate `Dispose` with `IEnumerable` was thus apparent before the release of `.net 1.0, but probably late enough that changing `IEnumerable` to inherit `IDisposable` would have impacted the release schedule. – supercat Jan 28 '13 at 18:23
-2

IIRC The whole thing about having IEnumerable<T> and IEnumerable is a result of IEnumerable predating .Net's template stuff. I suspect that your question is in the same way.

BCS
  • 75,627
  • 68
  • 187
  • 294