11

Just wondering why Enumerable.Range implements IDisposable.

I understand why IEnumerator<T> does, but IEnumerable<T> doesn't require it.


(I discovered this while playing with my .Memoise() implementation, which has statement like

if (enumerable is IDisposable)
    ((IDisposable)enumerable).Dispose();

in its "source finished" method that I had placed a breakpoint on out of curiousity, and was triggered by a test.)

Michael Petrotta
  • 59,888
  • 27
  • 145
  • 179
Fowl
  • 4,940
  • 2
  • 26
  • 43

1 Answers1

7

Enumerable.Range uses yield return in its method body. The yield return statement produces an anonymous type that implements IDisposable, under the magic of the compiler, like this:

static IEnumerable<int> GetNumbers()
{
    for (int i = 1; i < 10; i += 2)
    {
        yield return i;
    }
}

After being compiled, there is an anonymous nested class like this:

[CompilerGenerated]
private sealed class <GetNumbers>d__0 
   : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
    //the implementation
    //note the interface is implemented explicitly
    void IDisposable.Dispose() { }
}

so the result is a IDisposable. In this example, the Dispose method leaves empty. I think the reason is that there is nothing need to be disposed. If you yield return a type that contains unmanaged resources, you may get a different compiling result. (NOT SURE about it)

Cheng Chen
  • 42,509
  • 16
  • 113
  • 174
  • The compiler implements both IEnumerable and IEnumerator with the same class? Interesting. I'll have to think about that works. – Fowl Jul 04 '12 at 04:21
  • Oh, I see it now. It uses its state machine to detect if GetEnumerator has been called multiple times and returns itself if it hasn't. – Fowl Jul 04 '12 at 04:33
  • That makes sense, but having a factory method return a type which implements `IDisposable` when the return type of the factory does not would seem to be an anti-pattern. Compiler-generated iterators produced via `yield return` would appear to be types where abandonment without disposal would in fact be harmless provided `GetEnumerator()` has never been called, but once `GetEnumerator()` has been called, disposal is generally necessary. Incidentally, calling `Dispose` on an iterator after `GetEnumerable()` has been called at least once will invalidate the first enumerator only. – supercat Jul 05 '12 at 18:08
  • Is calling `Dispose` on a iterator defined to invalidate (or not) all of its enumerators though? Either way having different behaviour for the first `GetEnumerable` call is unexpected. Premature optimisation by the compiler? – Fowl Jul 06 '12 at 00:23
  • (To clarify @supercat, calling `Dispose` on a compiler generated `IEnumerable` implementation is *never* necessary, unless you're mishandling the iterator.) – Fowl Jul 06 '12 at 01:16
  • @Fowl: If an iterator performs a return within the `try` portion of a `try`/`finally` block, the `finally` portion will only execute when the iterator is disposed. – supercat Jul 06 '12 at 06:09
  • @supercat We're not disagreeing? – Fowl Jul 06 '12 at 06:10