Подтвердить что ты не робот

Ошибка в методе File.ReadLines(..).NET Framework 4.0

Этот код:

IEnumerable<string> lines = File.ReadLines("file path");
foreach (var line in lines)
{
    Console.WriteLine(line); 
}
foreach (var line in lines)
{ 
    Console.WriteLine(line); 
} 

выбрасывает ObjectDisposedException : {"Cannot read from a closed TextReader."}, если выполняется второй foreach. Кажется, что объект-итератор, возвращенный из File.ReadLines(..), не может быть перечислит более одного раза. Вы должны получить новый объект итератора, вызвав File.ReadLines(..), а затем использовать его для итерации.

Если заменить File.ReadLines(..) на мою версию (параметры не проверены, это просто пример):

public static IEnumerable<string> MyReadLines(string path)
{
    using (var stream = new TextReader(path))
    {
        string line;
        while ((line = stream.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

возможно повторять несколько строк строки файла.

Исследование с использованием .Net Reflector показало, что реализация File.ReadLines(..) вызывает частный File.InternalReadLines(TextReader reader), который создает фактический итератор. Читатель, переданный как параметр, используется в методе MoveNext() итератора для получения строк файла и расположен, когда мы доходим до конца файла. Это означает, что как только MoveNext() возвращает false, нет другого способа повторить итерацию второй раз, потому что читатель закрыт, и вы должны получить нового читателя, создав новый итератор с помощью метода ReadLines(..). В моей версии новый читатель созданный в методе MoveNext() каждый раз при запуске новой итерации.

Является ли это ожидаемым поведением метода File.ReadLines(..)?

Я нахожу тревогу, что нужно каждый раз вызывать метод, прежде чем перечислять результаты. Вам также нужно будет вызывать метод каждый раз, прежде чем вы будете перебирать результаты запроса Linq, который использует этот метод.

4b9b3361

Ответ 1

Я знаю, что это старо, но на самом деле я просто столкнулся с этим, работая над некоторым кодом на машине под Windows 7. Вопреки тому, что говорили здесь люди, эта была ошибкой. См. эту ссылку.

Итак, легкое исправление заключается в обновлении вашей фреймворка .net. Я думал, что это стоит обновить, так как это был лучший результат поиска.

Ответ 2

Я не думаю, что это ошибка, и я не думаю, что это необычно - на самом деле это то, что я ожидаю от чего-то вроде текстового читателя. IO - дорогостоящая операция, поэтому в общем вы хотите сделать все за один проход.

Ответ 3

Это не ошибка. Но я считаю, что вы можете использовать ReadAllLines(), чтобы делать то, что вам нужно. ReadAllLines создает строковый массив и втягивает все строки в массив, а не просто простой перечислитель поверх потока, например ReadLines.

Ответ 4

Если вам нужно дважды получить доступ к строкам, вы всегда можете их буферизовать в List<T>

using System.Linq;

List<string> lines = File.ReadLines("file path").ToList(); 
foreach (var line in lines) 
{ 
    Console.WriteLine(line);  
} 
foreach (var line in lines) 
{  
    Console.WriteLine(line);  
} 

Ответ 5

Я не знаю, можно ли это считать ошибкой или нет, если по дизайну, но я могу сказать две вещи...

  • Это должно быть опубликовано в Connect, а не в StackOverflow, хотя они не собираются изменять его до выпуска 4.0. И это обычно означает, что они никогда не смогут это исправить.
  • Конструкция метода, безусловно, представляется ошибочной.

Вы правильно заметили, что возврат IEnumerable означает, что он должен быть повторно использован, и он не гарантирует тех же результатов, если повторить дважды. Если бы он вернул IEnumerator, тогда это будет другая история.

Так или иначе, я думаю, что это хорошая находка, и я думаю, что API - это отвратительный. ReadAllLines и ReadAllText дают вам удобный способ получить доступ ко всему файлу, но если вызывающий абонент достаточно заботится о производительности, чтобы использовать ленивый перечислимый, они не должны делегировать так много ответственности за статический вспомогательный метод в первую очередь.

Ответ 6

Я считаю, что вы запутываете IQueryable с помощью IEnumerable. Да, верно, что IQueryable можно рассматривать как IEnumerable, но они не совсем то же самое. Запросы IQueryable каждый раз, когда он используется, в то время как IEnumerable не имеет такого подразумеваемого повторного использования.

Запрос Linq возвращает IQueryable. ReadLines возвращает IEnumerable.

Здесь тонкое различие связано с тем, как создается счетчик. IQueryable создает IEnumerator, когда вы вызываете GetEnumerator() на нем (что делается автоматически через foreach). ReadLines() создает IEnumerator при вызове функции ReadLines(). Таким образом, при повторном использовании IQueryable он создает новый IEnumerator при повторном использовании, но поскольку ReadLines() создает IEnumerator (а не IQueryable), единственный способ получить новый IEnumerator - снова вызвать ReadLines().

Другими словами, вы должны быть в состоянии ожидать повторного использования IQueryable, а не IEnumerator.

EDIT:

При дальнейших размышлениях (без каламбура) я думаю, что мой первоначальный ответ был слишком упрощенным. Если IEnumerable не был повторно использован, вы не могли бы сделать что-то вроде этого:

List<int> li = new List<int>() {1, 2, 3, 4};

IEnumerable<int> iei = li;

foreach (var i in iei) { Console.WriteLine(i); }
foreach (var i in iei) { Console.WriteLine(i); }

Очевидно, нельзя было ожидать, что второй foreach потерпит неудачу.

Проблема, как это часто бывает с этими видами абстракций, заключается в том, что не все идеально подходит. Например, потоки обычно являются однонаправленными, но для использования в сети они должны быть адаптированы для двунаправленной работы.

В этом случае IEnumerable изначально предполагалось, что это функция многократного использования, но с тех пор она была адаптирована так, чтобы быть универсальной, что повторное использование не является гарантией или даже должно ожидаться. Свидетельствуйте о взрыве различных библиотек, которые используют IEnumerables в вариантах, недоступных для повторного использования, например, в библиотеке Jeffery Richters PowerThreading.

Я просто не думаю, что мы можем предположить, что IEnumerables могут быть повторно использованы во всех случаях.

Ответ 7

Это не ошибка. File.ReadLines() использует ленивую оценку, и это не idempotent. Поэтому небезопасно перечислять его дважды подряд. Помните, что IEnumerable представляет источник данных, который можно перечислить, он не заявляет, что его можно перечислить дважды, хотя это может быть неожиданным, поскольку большинство людей используется для использования IEnumerable над идемпотентными коллекциями.

Из MSDN:

ReadLines (String, System) и Методы ReadAllLines (String, System) отличаются следующим образом: когда вы используете ReadLines, вы можете начать перечисление сбор строк перед возвращается вся коллекция; когда ты используйте ReadAllLines, вы должны подождать возвращается весь массив строк прежде чем вы сможете получить доступ к array.Therefore, когда вы работаете с очень большими файлами ReadLines может быть более эффективным.

Ваши результаты с помощью рефлектора правильны и проверяют это поведение. Внедрение, которое вы предоставили, позволяет избежать этого неожиданного поведения, но все еще использует ленивую оценку.