pm100
pm100

Reputation: 50110

'yield' enumerations that don't get 'finished' by caller - what happens

suppose I have

IEnumerable<string> Foo()
{
     try
     {

         /// open a network connection, start reading packets
         while(moredata)
         {
            yield return packet; 
        }
     }
     finally
      {
        // close connection 
      }
}

(Or maybe I did a 'using' - same thing). What happens if my caller goes

var packet = Foo().First();

I am just left with a leaked connection. When does the finally get invoked? Or does the right thing always happen by magic

edit with answer and thoughts

My sample and other 'normal' (foreach, ..) calling patterns will work nicely because they dispose of the IEnumerable (actually the IEnumerator returned by GetEnumerator). I must therefore have a caller somewhere thats doing something funky (explicitly getting an enumerator and not disposing it or the like). I will have them shot

the bad code

I found a caller doing

IEnumerator<T> enumerator = foo().GetEnumerator();

changed to

using(IEnumerator<T> enumerator = foo().GetEnumerator())

Upvotes: 35

Views: 1723

Answers (4)

theB
theB

Reputation: 6738

Ok this question could use a little empirical data.

Using VS2015 and a scratch project, I wrote the following code:

private IEnumerable<string> Test()
{
    using (TestClass t = new TestClass())
    {
        try
        {
            System.Diagnostics.Debug.Print("1");
            yield return "1";
            System.Diagnostics.Debug.Print("2");
            yield return "2";
            System.Diagnostics.Debug.Print("3");
            yield return "3";
            System.Diagnostics.Debug.Print("4");
            yield return "4";
        }
        finally
        {
            System.Diagnostics.Debug.Print("Finally");
        }
    }
}

private class TestClass : IDisposable
{
    public void Dispose()
    {
        System.Diagnostics.Debug.Print("Disposed");
    }
}

And then called it two ways:

foreach (string s in Test())
{
    System.Diagnostics.Debug.Print(s);
    if (s == "3") break;
}

string f = Test().First();

Which produces the following debug output

1
1
2
2
3
3
Finally
Disposed
1
Finally
Disposed

As we can see, it executes both the finally block and the Dispose method.

Upvotes: 22

ach
ach

Reputation: 2373

There is no special magic. If you check the doc on IEnumerator<T>, you'll find that it inherits from IDisposable. The foreach construct, as you know, is syntactic sugar which is decomposed by the compiler into a sequence of operations on an enumerator, and the whole thing is wrapped into a try/finally block, calling the Dispose on enumerator object.

When the compiler converts an iterator method (i. e. method containing yield statements) into an implementation of IEnumerable<T>/IEnumerator<T>, it handles the try/finally logic in the Dispose method of the generated class.

You might try to use ILDASM to analyze the code generated in your case. It's going to be pretty complex but it'll give you the idea.

Upvotes: 1

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

You would not end up with leaked connection. Iterator objects produced by yield return are IDisposable, and LINQ functions are careful to ensure proper disposal.

For example, First() is implemented as follows:

public static TSource First<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    IList<TSource> list = source as IList<TSource>;
    if (list != null) {
        if (list.Count > 0) return list[0];
    }
    else {
        using (IEnumerator<TSource> e = source.GetEnumerator()) {
            if (e.MoveNext()) return e.Current;
        }
    }
    throw Error.NoElements();
}

Note how the result of source.GetEnumerator() is wrapped in using. This ensures the call to Dispose, which in turn ensures the call of your code in the finally block.

Same goes for iterations by foreach loop: the code ensures disposal of the enumerator regardless of whether the enumeration completes or not.

The only case when you may end up with leaked connection is when you call GetEnumerator yourself, and fail to properly dispose of it. However, this is a mistake in the code using IEnumerable, not in the IEnumerable itself.

Upvotes: 27

Servy
Servy

Reputation: 203804

I am just left with a leaked connection.

No, you're not.

When does the finally get invoked?

When the IEnumerator<T> is disposed, which First is going to do after getting the first item of the sequence (just like everyone should be doing when they use an IEnumerator<T>).

Now if someone wrote:

//note no `using` block on `iterator`
var iterator = Foo().GetEnumerator();
iterator.MoveNext();
var first = iterator.Current;
//note no disposal of iterator

then they would leak the resource, but there the bug is in the caller code, not the iterator block.

Upvotes: 36

Related Questions