Почему IEumerator <T> влияет на состояние IEnumerable <T>, даже перечислитель никогда не доходил до конца?

Мне любопытно, почему следующее выдает сообщение об ошибке (закрытое исключение для чтения текста) по "последнему" назначению:

IEnumerable<string> textRows = File.ReadLines(sourceTextFileName);
IEnumerator<string> textEnumerator = textRows.GetEnumerator();

string first = textRows.First();
string last = textRows.Last();

Однако выполняется следующее:

IEnumerable<string> textRows = File.ReadLines(sourceTextFileName);

string first = textRows.First();
string last = textRows.Last();

IEnumerator<string> textEnumerator = textRows.GetEnumerator();

В чем причина различного поведения?

Ответ 1

Вы обнаружили ошибку в структуре, насколько я могу судить. Это разумно тонко, из-за взаимодействия нескольких вещей:

  • Когда вы вызываете ReadLines(), файл фактически открывается. Лично я думаю об этом как о самой ошибке; Я ожидал бы и надеюсь, что это будет лениво - только открытие файла, когда вы пытаетесь начать повторять его.
  • Когда вы вызываете GetEnumerator() первый раз при возврате значения ReadLines, он фактически вернет ту же ссылку.
  • Когда First() вызывает GetEnumerator(), он создаст клон. Это будет иметь тот же StreamReader, что и textEnumerator
  • Когда First() удаляет свой клон, он утилизирует StreamReader и устанавливает свою переменную в null. Это не влияет на переменную в оригинале, которая теперь относится к расположенному StreamReader
  • Когда Last() вызывает GetEnumerator(), он создаст клон исходного объекта, в комплекте с которыми находится StreamReader. Затем он пытается прочитать этот читатель и выдает исключение.

Теперь сравните это со своей второй версией:

  • Когда First() вызывает GetEnumerator(), возвращается исходная ссылка с открытым считывателем.
  • Когда First() затем вызывает Dispose(), читатель будет удален, а переменная установлена ​​на null
  • Когда Last() вызывает GetEnumerator(), будет создан клон, но поскольку значение, которое оно клонирует, имеет ссылку null, создается новый StreamReader, поэтому он может без проблем читать файл. Затем он располагает клоном, который закрывает читателя.
  • Когда вызывается GetEnumerator(), второй клон исходного объекта, открывающий еще один StreamReader - опять же, проблем нет.

Таким образом, проблема в первом фрагменте заключается в том, что вы вызываете GetEnumerator() второй раз (в First()), не удаляя первый объект.

Вот еще один пример той же проблемы:

using System;
using System.IO;
using System.Linq;

class Test
{
    static void Main()
    {
        var lines = File.ReadLines("test.txt");
        var query = from x in lines
                    from y in lines
                    select x + "/" + y;
        foreach (var line in query)
        {
            Console.WriteLine(line);
        }
    }
}

Вы можете исправить это, вызвав File.ReadLines дважды - или используя истинно ленивую реализацию ReadLines, например:

using System.IO;
using System.Linq;

class Test
{
    static void Main()
    {
        var lines = ReadLines("test.txt");
        var query = from x in lines
                    from y in lines
                    select x + "/" + y;
        foreach (var line in query)
        {
            Console.WriteLine(line);
        }
    }

    static IEnumerable<string> ReadLines(string file)
    {
        using (var reader = File.OpenText(file))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                yield return line;
            }
        }
    }
}

В последнем коде новый StreamReader открывается каждый раз, когда вызывается GetEnumerator(), поэтому результатом является каждая пара строк в test.txt.