Search code examples
c#ienumerabledisposeyield-return

Is dispose called down to the bottom via yield return?


I give silly examples for simplicity.

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    foreach(var x in source) yield return x;
}

I know that this will be compiled into a state machine. but its also similar to

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    using(var sillier = source.GetEnumerator())
    {
        while(sillier.MoveNext()) yield return sillier.Current;
    }
}

Now consider this usage

list.Silly().Take(2).ToArray();

Here you can see that Silly enumerable may not be fully consumed, but Take(2) it self will be fully consumed.

Question: when dispose is called on Take enumerator will it also call dispose on Silly enumerator and more specifically sillier enumerator?

My guess is, compiler can handle this simple use case because of foreach but what about not so simple use cases?

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    using(var sillier = source.GetEnumerator())
    {
        // move next can be called on different stages.
    }
}

Will this ever be a problem? because most enumerators don't use unmanaged resources, but if one does, this can cause memory leaks.


If dispose is not called, How do i make disposable enumerable?


An Idea: there can be a if(disposed) yield break; after every yield return. now dispose method of silly enumerator will just have to set disposed = true and move the enumerator once to dispose all the required stuff.


Solution

  • The C# compiler takes care of a lot for you when it turns your iterator into the real code. For instance, here's the MoveNext which contains the implementation of your second example1:

    private bool MoveNext()
    {
        try
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<sillier>5__1 = this.source.GetEnumerator();
                    this.<>1__state = -3;
                    while (this.<sillier>5__1.MoveNext())
                    {
                        this.<>2__current = this.<sillier>5__1.Current;
                        this.<>1__state = 1;
                        return true;
                    Label_005A:
                        this.<>1__state = -3;
                    }
                    this.<>m__Finally1();
                    this.<sillier>5__1 = null;
                    return false;
    
                case 1:
                    goto Label_005A;
            }
            return false;
        }
        fault
        {
            this.System.IDisposable.Dispose();
        }
    }
    

    So, you'll notice that the finally clause from your using isn't there at all, and it's a state machine2 that relies on being in certain good (>= 0) states in order to make further progress forwards. (It's also illegal C#, but hey ho).

    Now lets look at its Dispose:

    [DebuggerHidden]
    void IDisposable.Dispose()
    {
        switch (this.<>1__state)
        {
            case -3:
            case 1:
                try
                {
                }
                finally
                {
                    this.<>m__Finally1();
                }
                break;
        }
    }
    

    So we can see the <>m__Finally1 is called here (as well as due to exiting the while loop in MoveNext.

    And <>m__Finally1:

    private void <>m__Finally1()
    {
        this.<>1__state = -1;
        if (this.<sillier>5__1 != null)
        {
            this.<sillier>5__1.Dispose();
        }
    }
    

    So, we can see that sillier was disposed and we moved into a negative state which means that MoveNext doesn't have to do any special work to handle the "we've already been disposed state".

    So,

    An Idea: there can be a if(disposed) yield break; after every yield return. now dispose method of silly enumerator will just have to set disposed = true and move the enumerator once to dispose all the required stuff.

    Is completely unnecessary. Trust the compiler to transform the code so that it does all of the logical things it should - it just runs it's finally clause once, when it's either exhausted the iterator logic or when it's explicitly disposed.


    1All code samples produced by .NET Reflector. But it's too good at decompiling these constructs these days so if you go and look at the Silly method itself:

    [IteratorStateMachine(typeof(<Silly>d__1)), Extension]
    private static IEnumerable<T> Silly<T>(this IEnumerable<T> source)
    {
        IEnumerator<T> <sillier>5__1;
        using (<sillier>5__1 = source.GetEnumerator())
        {
            while (<sillier>5__1.MoveNext())
            {
                yield return <sillier>5__1.Current;
            }
        }
        <sillier>5__1 = null;
    }
    

    It's managed to hide most details about that state machine away again. You need to chase the type referenced by the IteratorStateMachine attribute to see all of the gritty bits shown above.


    2Please also note that the compiler is under no obligations to produce a state machine to allow iterators to work. It's an implementation detail of the current C# compilers. The C# Specification places no restriction on how the compiler transforms the iterator, just on what the effects should be.