Почему 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.

Другие вопросы по тегам