Почему 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();

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


person Matt    schedule 26.12.2012    source источник
comment
На самом деле оба кода дают сбой на моей машине...   -  person digEmAll    schedule 26.12.2012
comment
@digEmAll, у меня второй работает нормально, первый код ломается, когда я пытаюсь определить последнюю строку в текстовом файле.   -  person Matt    schedule 26.12.2012
comment
@digEmAll: Странно — у меня второй код работает нормально, и я понимаю, почему он работает нормально. Какую проблему вы видите и где?   -  person Jon Skeet    schedule 26.12.2012
comment
@JonSkeet, у меня тоже второй код не работает, с той же ошибкой и в той же строке.   -  person Andrei    schedule 26.12.2012
comment
@Андрей: Хм. Какую версию .NET вы используете? Это в отладчике?   -  person Jon Skeet    schedule 26.12.2012
comment
@JonSkeet, версия фреймворка 4. Профиль клиента, если это имеет значение. Ошибка появляется как с отладкой, так и без нее.   -  person Andrei    schedule 26.12.2012
comment
@Андрей: Странно. Я использую .NET 4.5, но я удивлен, увидев разницу. Хм.   -  person Jon Skeet    schedule 26.12.2012
comment
@JonSkeet: я также использую .net 4 (на моем ПК не установлена ​​4.5), и, глядя на декомпилированный код, я не удивлен, что оба не работают ... может быть, что-то изменилось в 4.5 ...   -  person digEmAll    schedule 26.12.2012


Ответы (1)


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

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

person Jon Skeet    schedule 26.12.2012
comment
Спасибо за подробное объяснение. Довольно интересно, мотивирует меня копать глубже под капотом, так как я не знал, что First() или Last() размещают внутренний итератор/перечислитель. - person Matt; 26.12.2012
comment
@Freddy: Все, в котором используется IEnumerator<T>, должно его утилизировать. - person Jon Skeet; 26.12.2012
comment
Я понимаю вашу точку зрения, но в этом случае, вызывая First() и Last(), я бы хотел, чтобы он не избавлялся от основного средства чтения текста. В этом я все еще немного запутался, с одной стороны, нужно вручную удалять, с другой стороны, объект сам удаляется после вызова First() или Last()... - person Matt; 26.12.2012
comment
@JonSkeet: я только что посмотрел исходный код .net 4.5. Они изменили код, основанный на доходности, с реализацией IEnumerator, пытаясь сохранить то же поведение. Во всяком случае, я скопировал код в проект .net 4.0, и второй случай работает, а при использовании .net 4 File.ReadLines это не так... - person digEmAll; 27.12.2012
comment
@Freddy: проблема не в утилизации, а в том, что вызов GetEnumerator должен всегда создавать новый считыватель IMO. - person Jon Skeet; 27.12.2012
comment
@JonSkeet, да тогда все было бы ясно. Новый перечислитель создаст новый считыватель, а вызов First(), Last() не создаст и не удалит. - person Matt; 27.12.2012
comment
@Freddy: Нет, вызов First() или Last() создаст (через GetEnumerator()), затем прочитает, а затем утилизирует. - person Jon Skeet; 27.12.2012
comment
@JonSkeet, хорошо, понял, Last () или First () - это методы, в конце концов, если бы они были свойствами или полями, мне было бы странно, что был создан новый читатель. Не будет ли намного лучше создать перечислитель и, следовательно, средство чтения при создании экземпляра IEnumerable, учитывая, что перечислитель перечисляет поток? Тогда доступ к перечислителю через Last() или First() или что-то еще не будет мешать основному потоку. - person Matt; 27.12.2012
comment
@Freddy: Ну, ему пришлось бы мешать основному потоку, иначе он не смог бы получить данные. Но я бы сказал, что перечислитель перечисляет файл, а не поток. Это подход, который LINQ использует повсеместно: запрос — это просто представление запроса; никакие данные не извлекаются (и GetEnumerator не вызывается), пока они не потребуются. Вся проблема здесь в том, что ридер создается раньше, чем нужно, ИМО. Если бы каждый вызов GetEnumerator создавал новый считыватель, все было бы хорошо. - person Jon Skeet; 27.12.2012
comment
@JonSkeet, справедливо, но не вызовут ли проблемы одновременные считыватели, указывающие на один и тот же открытый файл? - person Matt; 27.12.2012
comment
@Freddy: Нет - посмотрите код в конце моего ответа, чтобы увидеть пример, который отлично работает. - person Jon Skeet; 27.12.2012
comment
@JonSkeet, извините за мою формулировку, я знал, что это может сработать, я думаю, я хотел сказать, что найду дизайн, в котором читатель сидит с IEnumerable, будет иметь для меня гораздо больше смысла. Средство чтения будет создано только при первом вызове GetEnumerator. Последующее использование IEnumerator не приведет к созданию другого средства чтения. Если я сравнил это с перемещением по бинарному потоку только потому, что я установил позицию потока с помощью Seek(), это не означает, что мне нужно создать новый бинарный читатель или писатель. Аналогично с получением первого и последнего элемента IEnumerable... - person Matt; 27.12.2012
comment
... мне трудно понять, зачем нам несколько читателей. По крайней мере, я не вижу преимущества, потому что GetEnumerator() разделяет отношения с IEnumerable. Я, очевидно, знаю, что текущий дизайн отличается. Извиняюсь за то, что иногда искажаю жаргон, я программист-самоучка, и это только с тех пор, как 1,5 года - person Matt; 27.12.2012
comment
@JonSkeet, спасибо за объяснения, я могу многому у тебя научиться. - person Matt; 27.12.2012
comment
@Freddy: Нет, каждый вызов GetEnumerator() должен создавать независимый итератор. Поэтому, если вы вызываете GetEnumerator() несколько раз, он должен создать несколько курсоров для данных, независимо от источника данных. Вот почему IEnumerator<T> существует в первую очередь. - person Jon Skeet; 27.12.2012
comment
@JonSkeet, я это понимаю, но мне трудно сопоставить средство чтения текста с простой конструкцией, такой как курсор/итератор. Для настройки сравнения курсор/итератор для меня это как закладка, книга как источник данных. Зачем клонировать/дублировать книги, когда книга может быть прочитана одним объектом за раз (из-за ограничений ввода-вывода здесь). Просто потому, что я говорю курсору/итератору идти куда-то, почему это требует настройки нового считывателя, для меня очень мало смысла. - person Matt; 27.12.2012
comment
@JonSkeet, да, я согласен с вашими точками зрения, но я не согласен с тем, почему для каждого нового счетчика создается новый читатель. Каким бы ни был источник данных -> Полностью согласен, тогда зачем создавать полное устройство чтения (считыватель), когда вы просто пытаетесь перебрать один и тот же источник данных. Но это попадает в дискуссию, остается мое мнение, что один и ровно один ридер должен быть создан независимо от того, сколько Итераторов я получу, как это можно будет реализовать и устранит ли он все подводные камни, в один из которых я попал, я не знаю. Еще раз спасибо за ваш глубокий анализ и код. С Новым Годом. - person Matt; 29.12.2012
comment
@Freddy: чтобы соответствовать нормальному ожидаемому поведению, что-то вроде File.ReadLines() должно либо возвращать объект, который создает новый считыватель при каждом вызове GetEnumerator(), либо создавать объект, перечислители которого имеют логику для совместного использования считывателя в потоке. -safe fashion и очистите его, когда все закончат с ним. Первый подход намного проще. - person supercat; 13.03.2013