Ранний выход из вызывающего кода при использовании yield return для универсального IEnumerable - PullRequest
2 голосов
/ 24 октября 2019

Что происходит, когда вызывающий код завершается до завершения перечисления 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закрыто своевременно. Нужен ли шаблон для обработки этого сценария?

1 Ответ

3 голосов
/ 24 октября 2019

Хороший вопрос.

Видите ли, при использовании foreach () для итерации получаемого IEnumerable вы в безопасности. Приведенный ниже Enumerator сам реализует IDisposable, который вызывается в случае foreach (даже если цикл завершается с разрывом) и заботится об очистке вашего состояния в 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 ()) как «ошибку», но это подразумеваемая реализация.

...