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