Reputation: 50110
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
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
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
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
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