Why disposing StreamReader makes a stream unreadab

2019-04-21 05:25发布

问题:

This question already has an answer here:

  • Can you keep a StreamReader from disposing the underlying stream? 8 answers

I need to read a stream two times, from start to end.

But the following code throws an ObjectDisposedException: Cannot access a closed file exception.

string fileToReadPath = @"<path here>";
using (FileStream fs = new FileStream(fileToReadPath, FileMode.Open))
{
    using (StreamReader reader = new StreamReader(fs))
    {
        string text = reader.ReadToEnd();
        Console.WriteLine(text);
    }

    fs.Seek(0, SeekOrigin.Begin); // ObjectDisposedException thrown.

    using (StreamReader reader = new StreamReader(fs))
    {
        string text = reader.ReadToEnd();
        Console.WriteLine(text);
    }
}

Why is it happening? What is really disposed? And why manipulating StreamReader affects the associated stream in this way? Isn't it logical to expect that a seekable stream can be read several times, including by several StreamReaders?

回答1:

This happens because the StreamReader takes over 'ownership' of the stream. In other words, it makes itself responsible for closing the source stream. As soon as your program calls Dispose or Close (leaving the using statement scope in your case) then it will dispose the source stream as well. Calling fs.Dispose() in your case. So the file stream is dead after leaving the first using block. It is consistent behavior, all stream classes in .NET that wrap another stream behave this way.

There is one constructor for StreamReader that allows saying that it doesn't own the source stream. It is however not accessible from a .NET program, the constructor is internal.

In this particular case, you'd solve the problem by not using the using-statement for the StreamReader. That's however a fairly hairy implementation detail. There's surely a better solution available to you but the code is too synthetic to propose a real one.



回答2:

The purpose of Dispose() is to clean up resources when you're finished with the stream. The reason the reader impacts the stream is because the reader is just filtering the stream, and so disposing the reader has no meaning except in the context of it chaining the call to the source stream as well.

To fix your code, just use one reader the entire time:

using (FileStream fs = new FileStream(fileToReadPath, FileMode.Open))
using (StreamReader reader = new StreamReader(fs))
{
    string text = reader.ReadToEnd();
    Console.WriteLine(text);

    fs.Seek(0, SeekOrigin.Begin); // ObjectDisposedException not thrown now

    text = reader.ReadToEnd();
    Console.WriteLine(text);
}

Edited to address comments below:

In most situations, you do not need to access the underlying stream as you do in your code (fs.Seek). In these cases, the fact that StreamReader chains its call to the underlying stream allows you to economize on the code by not using a usings statement for the stream at all. For example, the code would look like:

using (StreamReader reader = new StreamReader(new FileStream(fileToReadPath, FileMode.Open)))
{
    ...
}


回答3:

Using defines a scope, outside of which an object will be disposed, thus the ObjectDisposedException. You can't access the StreamReader's contents outside of this block.



回答4:

I agree with your question. The biggest issue with this intentional side-effect is when developers don't know about it and are blindly following the "best practice" of surrounding a StreamReader with a using. But it can cause some really hard to track down bugs when it is on a long-lived object's property, the best (worst?) example I've seen is

using (var sr = new StreamReader(HttpContext.Current.Request.InputStream))
{
    body = sr.ReadToEnd();
}

The developer had no idea the InputStream is now hosed for any future place that expects it to be there.

Obviously, once you know the internals you know to avoid the using and just read and reset the position. But I thought a core principle of API design was to avoid side effects, especially not destroying data you are acting upon. Nothing inherent about a class that supposedly is a "reader" should clear the data it reads when done "using" it. Disposing of the reader should release any references to the Stream, not clear the stream itself. The only thing I can think is that the choice had to be made since the reader is altering other internal state of the Stream, like the position of the seek pointer, that they assumed if you are wrapping a using around it you are intentionally going to be done with everything. On the other hand, just like in your example, if you are creating a Stream, the stream itself will be in a using, but if you are reading a Stream that was created outside of your immediate method, it is presumptuous of the code to clear the data.

What I do and tell our developers to do on Stream instances that the reading code doesn't explicitly create is...

// save position before reading
long position = theStream.Position;
theStream.Seek(0, SeekOrigin.Begin);
// DO NOT put this StreamReader in a using, StreamReader.Dispose() clears the stream
StreamReader sr = new StreamReader(theStream);
string content = sr.ReadToEnd();
theStream.Seek(position, SeekOrigin.Begin);

(sorry I added this as an answer, wouldn't fit in a comment, I would love more discussion about this design decision of the framework)



回答5:

Dispose() on parent will Dispose() all owned streams. Unfortunately, streams don't have Detach() method, so you have to create some workaround here.



回答6:

I don't know why, but you can leave your StreamReader undisposed. That way your underlying stream won't be disposed, even when StreamReader got collected.