tags:

views:

317

answers:

7

This code :

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

throws an ObjectDisposedException : {"Cannot read from a closed TextReader."} if the second foreach is executed. It seems that the iterator object returned from File.ReadLines(..) can't be enumerated more than once. You have to obtain a new iterator object by calling File.ReadLines(..) and then use it to iterate.

If I replace File.ReadLines(..) with my version(parameters are not verified, it's just an example):

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

it's possible to iterate more than once the lines of the file.

An investigation using .Net Reflector showed that the implementation of the File.ReadLines(..) calls a private File.InternalReadLines(TextReader reader) that creates the actual iterator. The reader passed as a parameter is used in the MoveNext() method of the iterator to get the lines of the file and is disposed when we reach the end of the file. This means that once MoveNext() returns false there is no way to iterate a second time because the reader is closed and you have to get a new reader by creating a new iterator with the ReadLines(..) method.In my version a new reader is created in the MoveNext() method each time we start a new iteration.

Is this the expected behavior of the File.ReadLines(..) method?

I find troubling the fact that it's necessary to call the method each time before you enumerate the results. You would also have to call the method each time before you iterate the results of a Linq query that uses the method.

+5  A: 

I don't think it's a bug, and I don't think it's unusual -- in fact that's what I'd expect for something like a text file reader to do. IO is an expensive operation, so in general you want to do everything in one pass.

jmoreno
Yes, but the reader could be created in the IEnumerable.GetEnumerator call, i.e. when enumeration begins, not when the IEnumerable is created. I agree with Adrian, that would be more predictable behaviour, and easier to use with the LINQ operators which the new method is intended to support (and more consistent with those LINQ operators since they are lazy).
itowlson
A: 

If you need to access the lines twice you can always buffer them into a 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);  
} 
bendewey
Trouble is that this requires .NET to read the whole lot in *at once*, which may be very inefficient for a large file. The whole point of the ReadLines method was to avoid the need for this (which, as Stephen points out, is already adequately handled by ReadAllLines).
itowlson
I gain nothing if I buffer the results in a list. I might as well use ReadAllLines() which is not lazy and returns an array of strings. If the file to read is very big this operation would take very long. I have to wait the whole array(or list) of strings be returned before I can access the array(or the list).
Adrian Constantin
@Adrian, if you are parsing large files then I would avoid this.
bendewey
@bendewey, the purpose of this method is to be able to obtain an IEnumerable and then use the iterator in a Linq query. I don't necessarily need to parse the file. I might want to get the data from the file and transform or manipulate it with a Linq query.
Adrian Constantin
A: 

It isn't a bug. But I believe you can use ReadAllLines() to do what you want instead. ReadAllLines creates a string array and pulls in all the lines into the array, instead of just a simple enumerator over a stream like ReadLines does.

Stephen M. Redd
As I mentioned before there are cases when I would rather not wait for the whole array to be returned before I can use the data in the array. Typically this is the case when the files are big and you end up with a 100 MB array in the memory.I can start enumerating the lines before the whole collection is returned.
Adrian Constantin
I have rarely seen anyone fight getting good answers to a good question this hard. Clearly it is not a bug. The documentation explains the behavior, and the explanation matches the actual behavior. There are two methods, one allows simple un-buffered enumeration over a read-only stream. The other buffers the contents to an array for cases where you need a reusable buffer. The return types match this intent. The unbuffered one returns IEnumerable. The buffered one returns an array. This alone makes the intent of the two different methods pretty clear.
Stephen M. Redd
With an array, you cannot start an enumeration before the array is fully loaded. The array will change while you are iterating it, which is explicitly disallowed. You seem to be suggesting that you want to have a stream that you can treat like an array later. That's fine. There are objects like that, especially in various LINQ implementations. But this isn't what *these* particular methods do. Like anything, you can use these and similar methods to do the more complex thing you want. Just write a class that does things that way.
Stephen M. Redd
A: 

I don't know if it can be considered a bug or not if it's by design but I can certainly say two things...

  1. This should be posted on Connect, not StackOverflow although they're not going to change it before 4.0 is released. And that usually means they won't ever fix it.
  2. The design of the method certainly appears to be flawed.

You are correct in noting that returning an IEnumerable implies that it should be reusable and it does not guarantee the same results if iterated twice. If it had returned an IEnumerator instead then it would be a different story.

So anyway, I think it's a good find and I think the API is a lousy one to begin with. ReadAllLines and ReadAllText give you a nice convenient way of getting at the entire file but if the caller cares enough about performance to be using a lazy enumerable, they shouldn't be delegating so much responsibility to a static helper method in the first place.

Josh Einstein
IEnumerable doesn't imply reusability. It only implies the ability to get a simple enumerator. A lot of non-reusable forward only IEnumerables are in the framework. There are other interfaces that apply to most objects that are reusable or provide more than simple enumerations (IList for example).
Stephen M. Redd
I disagree. I was careful not to say "guarantee" because it does not. But it does certainly *imply* reusability. Even IEnumerator sort of implies reusability because of its Reset method. I would however expect that calling IEnumerable.GetEnumerator multiple times should not throw or return the same instance as that is how virtually every other IEnumerable behaves, including LINQ queries.
Josh Einstein
A: 

I believe you are confusing an IQueryable with an IEnumerable. Yes, it's true that IQueryable can be treated as an IEnumerable, but they are not exactly the same thing. An IQueryable queries each time it's used, while an IEnumerable has no such implied reuse.

A Linq Query returns an IQueryable. ReadLines returns an IEnumerable.

There's a subtle distinction here because of the way an Enumerator is created. An IQueryable creates an IEnumerator when you call GetEnumerator() on it (which is done automatically by foreach). ReadLines() creates the IEnumerator when the ReadLines() function is called. As such, when you reuse an IQueryable, it creates a new IEnumerator when you reuse it, but since the ReadLines() creates the IEnumerator (and not an IQueryable), the only way to get a new IEnumerator is to call ReadLines() again.

In other words, you should only be able to expect to reuse an IQueryable, not an IEnumerator.

EDIT:

On further reflection (no pun intended) I think my initial response was a bit too simplistic. If IEnumerable was not reusable, you couldn't do something like this:

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); }

Clearly, one would not expect the second foreach to fail.

The problem, as is so often the case with these kinds of abstractions, is that not everything fits perfectly. For example, Streams are typically one-way, but for network use they had to be adapted to work bi-directionally.

In this case, an IEnumerable was originally envisioned to be a reusable feature, but it has since been adapted to be so generic that reusability is not a guarantee or even should be expected. Witness the explosion of various libraries that use IEnumerables in non-reusable ways, such as Jeffery Richters PowerThreading library.

I simply don't think we can assume IEnumerables are reusable in all cases anymore.

Mystere Man
That might be the case but the documentation on MSDN(http://msdn.microsoft.com/en-us/library/dd383503(VS.100).aspx) does not specify explicitly that you should iterate only once. One would expect an exception be thrown while attempting to enumerate in the case of trying to modify the collection being iterated.
Adrian Constantin
@Adrian - Since when have we looked at documentation for what you can't do? You typically look at it for what you *CAN* do. Documentation, by it's very nature, is often incomplete so we are usually lucky if it tells us everthing that can be done. If it includes things that can't, that tends to be more of an annotation.
Mystere Man
A: 

It's not a bug. File.ReadLines() uses lazy evaluation and it is not idempotent. That's why it's not safe to enumerate it twice in a row. Remember an IEnumerable represents a data source that can be enumerated, it does not state it is safe to be enumerated twice, although this might be unexpected since most people are used to using IEnumerable over idempotent collections.

From the MSDN:

The ReadLines(String, System) and ReadAllLines(String, System) methods differ as follows: When you use ReadLines, you can start enumerating the collection of strings before the whole collection is returned; when you use ReadAllLines, you must wait for the whole array of strings be returned before you can access the array.Therefore, when you are working with very large files, ReadLines can be more efficient.

Your findings via reflector are correct and verify this behavior. The implementation you provided avoids this unexpected behavior but makes still use of lazy evaluation.

Johannes Rudolph
This would be the first and only example I have ever seen of an IEnumerable.GetEnumerator function that cannot be called more than once.
Jonathan Allen
We've been discussing this intensively on the morelinq project and decided to implement all our operators as idempotent. Consumers naturally asume IEnumerables can be enumerated more than once. Again, in this case it's not a bug, it's a feature.
Johannes Rudolph
The fact that you can't enumerate twice the IEnumerable returned by ReadLines(..) it's just an implementation detail. The exception is thrown in the MoveNext() method of the enumerator. My implementation uses the reader as a local variable and therefore you get a new TextReader each time you start to enumerate. Clearly the problem here is that you need a new TextReader once you finished an enumeration.I don't see any reason why a file would not be iterated more than once.
Adrian Constantin
@Jonathan Allen - There are lots of examples of enumerators that can't be enumerated more than once, though this is the first one i've seen in the framework itself. The IEnumerable idiom is now being used all over the place for, in effect, a mini state engine. See Jeffery Richters PowerThreading library for other examples.
Mystere Man
Well of course anyone can write something that has the correct interface but works in a starge fashion. That is why I turn a dim eye to ones not in the framework itself.
Jonathan Allen
I agree with Mystere. To many people just think IEnumerable == foreach.
Johannes Rudolph
A: 

So is there anyway to use ReadLines twice? I read through all of this and cannot get a solid answer.

For a 500 MB tab delimited text file it is important to not ReadAllLines into memory. But for a certain LINQ Query I need to parse it twice. Suggestions?

var lines = File.ReadLines(GeoNamesPath); var places= from line in lines.AsParallel() let lines =line.Split(new char[] { '\t' }) where lines[0]=='Type1' select { Name= lines[1]};

lines = File.ReadLines(GeoNamesPath); var placeChildren=from place in places from line in lines.AsParallel() let lines =line.Split(new char[] { '\t' }) where lines[0]=='Type2'&& lines[3]==place.Name select { Name= lines[1], Parent=place};

Jonathan Perl
I have posted the bug on the connect.microsoft.com site and i got a response from David Kean. I fact there are 2 issues with their implementation.
Adrian Constantin
1) Enumerators would share a single StreamReader. This would mean that having multiple enumerators at the same time would affect each other's state, for example, calling MoveNext on one enumerator would effectively call it for all enumerators. Calling Dispose on one enumerator would effectively call Dispose on all other enumerators running at the same time. This is in contrast to the contract of IEnumerable<T> where each call to GetEnumerator must return a self-contained independent instance of IEnumerator<T>.
Adrian Constantin
2) Once a enumerator had been disposed (either via explictly calling IEnumerator<T>.Dispose or by reading to the end of the underlying stream), it prevented future enumerators from reading from the file, because the underlying StreamReader would be disposed.
Adrian Constantin