Search code examples
c#usingyield-return

Can I use "using" in a yield-return-method?


I just saw a YouTube video, where the tutor used a yield return method to open a file and read lines from it, that are yield-returned to the caller (the actual code was in a using block around the FileStream).

Then I wondered, is it OK to use "using" or "try-finally" in a yield-return method. Because my understanding is, that the method only runs as long, as values are get from it. With "Any()" for example the method is done, after the first yield return (or yield break of course).

So, if the function never runs to end, when is the finally block executed? Is it safe to use such a construct?


Solution

  • IEnumerator<T> implements IDisposable, and foreach loops will dispose the thing they're enumerating over when they're finished (this includes linq methods which use a foreach loop, such as .ToArray()).

    It turns out that the compiler-generated state machine for generator methods implements Dispose in a smart way: if the state machine is in a state which is "inside" a using block, then calling Dispose() on the state machine will dispose the thing protected by the using statement.


    Let's take an example:

    public IEnumerable<string> M() {
        yield return "1";
        using (var ms = new MemoryStream())
        {
            yield return "2";  
            yield return "3";
        }
        yield return "4";
    }
    

    I'm not going to paste the entire generated state machine, as it's very large. You can see it on SharpLab here.

    The core of the state machine is the following switch statement, which tracks our progress past each of the yield return statements:

    switch (<>1__state)
    {
        default:
            return false;
        case 0:
            <>1__state = -1;
            <>2__current = "1";
            <>1__state = 1;
            return true;
        case 1:
            <>1__state = -1;
            <ms>5__1 = new MemoryStream();
            <>1__state = -3;
            <>2__current = "2";
            <>1__state = 2;
            return true;
        case 2:
            <>1__state = -3;
            <>2__current = "3";
            <>1__state = 3;
            return true;
        case 3:
            <>1__state = -3;
            <>m__Finally1();
            <ms>5__1 = null;
            <>2__current = "4";
            <>1__state = 4;
            return true;
        case 4:
            <>1__state = -1;
            return false;
    }
    

    You can see that we create the MemoryStream as we enter state 2, and dispose it (by calling <>m__Finally1()) as we exit state 3.

    Here's the Dispose method:

    void IDisposable.Dispose()
    {
        int num = <>1__state;
        if (num == -3 || (uint)(num - 2) <= 1u)
        {
            try
            {
            }
            finally
            {
                <>m__Finally1();
            }
        }
    }
    

    If we're in states -3, 2, or 3, then we'll call <>m__Finally1();. States 2 and 3 are those inside the using block.

    (State -3 seems to be a guard in case we wrote yield return Foo() and Foo() threw an exception: in this case we would stay in state -3 and would be unable to iterate any further. However we are still allowed to dispose the MemoryStream in this case).

    Just for completeness, <>m__Finally1 is defined as:

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

    You can find the specification for this in the C# Language Specification, section 10.14.4.3:

    • If the state of the enumerator object is suspended, invoking Dispose:
      • Changes the state to running.
      • Executes any finally blocks as if the last executed yield return statement were a yield break statement. If this causes an exception to be thrown and propagated out of the iterator body, the state of the enumerator object is set to after and the exception is propagated to the caller of the Dispose method.
      • Changes the state to after.