Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Early exit from calling code when using yield return on a generic IEnumerable

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?

like image 359
rediVider Avatar asked Feb 28 '26 07:02

rediVider


1 Answers

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.

like image 96
Dmytro Avatar answered Mar 01 '26 21:03

Dmytro