What happens when calling code exits prior to completing enumeration of a IEnumerable that is yield returning.
A simplified example:
public void HandleData()
{
int count = 0;
foreach (var datum in GetFileData())
{
//handle datum
if (++count > 10)
{
break;//early exit
}
}
}
public static IEnumerable<string> GetFileData()
{
using (StreamReader sr = _file.BuildStreamer())
{
string line = String.Empty;
while ((line = sr.ReadLine()) != null)
{
yield return line;
}
}
}
In this case it seems quite important that the StreamReader is closed in a timely manner. Is there a pattern needed to handle this scenario?
That's a good question.
You see, while using foreach() to iterate resulting IEnumerable, you're safe. The Enumerator below implements IDisposable itself, which gets called in case of foreach (even if loop is exited with break) and takes care of cleaning the state of your in GetFileData.
But if you will play with Enumerator.MoveNext directly, you're in trouble and Dispose will never be called if exited earlier (of course, if you'll complete manual iteration, it will be).For manual Enumerator-based iteration, you can consider placing enumerator in using statement as well (as mentioned in code below).
Hope this example with different usecases covered will provide you some feedback for your question.
static void Main(string[] args)
{
// Dispose will be called
foreach(var value in GetEnumerable())
{
Console.WriteLine(value);
break;
}
try
{
// Dispose will be called even here
foreach (var value in GetEnumerable())
{
Console.WriteLine(value);
throw new Exception();
}
}
catch // Lame
{
}
// Dispose will not be called
var enumerator = GetEnumerable().GetEnumerator();
// But if enumerator and this logic is placed inside the "using" block,
// like this: using(var enumerator = GetEnumerable().GetEnumerable(){}), it will be.
while(enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
break;
}
Console.WriteLine("{0}Here we'll see dispose on completion of manual enumeration.{0}", Environment.NewLine);
// Dispose will be called: ended enumeration
var enumerator2 = GetEnumerable().GetEnumerator();
while (enumerator2.MoveNext())
{
Console.WriteLine(enumerator2.Current);
}
}
static IEnumerable<string> GetEnumerable()
{
using (new MyDisposer())
{
yield return "First";
yield return "Second";
}
Console.WriteLine("Done with execution");
}
public class MyDisposer : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed");
}
}
Originally observed by: https://blogs.msdn.microsoft.com/dancre/2008/03/15/yield-and-usings-your-dispose-may-not-be-called/
Author calls this (the fact that manual MoveNext() and early break will not trigger Dipose()) "a bug", but this is intended implementation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With