在通用 IEnumerable 上使用 yield return 时提前退出调用代码

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

当调用代码在完成对正在返回的 IEnumerable 的枚举之前退出时会发生什么。

一个简化的例子:

    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;
            }
        }
    }

在这种情况下,及时关闭 StreamReader 似乎非常重要。是否需要一种模式来处理这种情况?

这是个好问题。

你看,在使用 foreach() 迭代生成的 IEnumerable 时,你是安全的。下面的 Enumerator 实现了 IDisposable 本身,它在 foreach 的情况下被调用(即使使用 break 退出循环)并负责清理 GetFileData 中的状态。

但是如果你直接玩Enumerator.MoveNext,你就有麻烦了,如果提前退出,Dispose 将永远不会被调用(当然,如果你将完成手动迭代,它将会)。对于手动基于枚举器的迭代,您也可以考虑将枚举器放在 using 语句中(如下面的代码所述)。

希望这个包含不同用例的示例能为您的问题提供一些反馈。

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");
    }
}

最初观察者:https://blogs.msdn.microsoft.com/dancre/2008/03/15/yield-and-usings-your-dispose-may-not-be-called/
作者称之为(手动 MoveNext() 和早期中断不会触发 Dipose() 的事实)"a bug",但这是预期的实现。