为什么处置 StreamReader 会使流变得不可读?
我需要从头到尾读取流两次。
但以下代码会引发 ObjectDisposeException: Cannot access a Closed file
异常。
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);
}
}
为什么会发生这种情况?真正处置的是什么?为什么操作 StreamReader
会以这种方式影响关联的流?期望可查找流可以被多次读取(包括由多个 StreamReader 读取),这不是合乎逻辑的吗?
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 StreamReader
s?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
发生这种情况是因为
StreamReader
接管了流的“所有权”。换句话说,它让自己负责关闭源流。一旦您的程序调用Dispose
或Close
(在您的情况下保留using
语句范围),它也会处理源流。在您的情况下调用fs.Dispose()
。因此,文件流在离开第一个using
块后就死了。这是一致的行为,.NET 中包装另一个流的所有流类都以这种方式运行。StreamReader
有一个构造函数,允许声明它不拥有源流。然而,它不能从 .NET 程序访问,构造函数是内部的。在这种特殊情况下,您可以通过不使用
StreamReader
的using
语句来解决该问题。然而,这是一个相当棘手的实现细节。肯定有更好的解决方案可供您使用,但代码过于综合,无法提出真正的解决方案。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 callsDispose
orClose
(leaving theusing
statement scope in your case) then it will dispose the source stream as well. Callingfs.Dispose()
in your case. So the file stream is dead after leaving the firstusing
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 theStreamReader
. 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.Dispose()
的目的是在完成流后清理资源。 读取器影响流的原因是读取器只是过滤流,因此处理读取器没有任何意义,除非在它将调用链接到源流也是如此。要修复您的代码,只需始终使用一个阅读器:
编辑以解决下面的注释:
在大多数情况下,您不需要像在代码中那样访问底层流 (
fs .寻求
)。在这些情况下,StreamReader
将其调用链接到底层流,这一事实使您可以通过根本不使用流的usings
语句来节省代码。例如,代码如下所示: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:
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 thatStreamReader
chains its call to the underlying stream allows you to economize on the code by not using ausings
statement for the stream at all. For example, the code would look like:Using
定义了一个范围,在该范围之外的对象将被释放,因此会出现ObjectDisposeException
。您无法访问此块之外的 StreamReader 内容。Using
defines a scope, outside of which an object will be disposed, thus theObjectDisposedException
. You can't access the StreamReader's contents outside of this block.我同意你的问题。这种故意的副作用的最大问题是,开发人员不知道这一点,并且盲目地遵循使用
using
围绕 StreamReader 的“最佳实践”。但是,当它位于长期存在的对象的属性上时,它可能会导致一些非常难以追踪的错误,我见过的最好(最差?)的例子是开发人员不知道InputStream现在已被用于任何未来期望的地方它在那里。
显然,一旦您了解了内部原理,您就知道要避免
using
并仅读取并重置位置。但我认为 API 设计的核心原则是避免副作用,尤其是不要破坏您正在操作的数据。一个被认为是“读取器”的类在“使用”完成后不应清除它读取的数据。处理 reader 应该释放对 Stream 的所有引用,而不是清除流本身。我唯一能想到的是,必须做出选择,因为读者正在改变流的其他内部状态,比如搜索指针的位置,他们假设如果你在它周围包装一个 using ,你会故意去一切都办完了。另一方面,就像在您的示例中一样,如果您正在创建一个 Stream,则该流本身将位于using
中,但如果您正在读取在直接方法之外创建的 Stream,代码清除数据是冒昧的。我所做的并告诉我们的开发人员在读取代码未显式创建的 Stream 实例上执行的操作是...
(抱歉,我将其添加为答案,不适合评论,我希望更多地讨论此设计框架的决定)
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 isThe 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 ausing
, 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...
(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)
父级上的
Dispose()
将Dispose()
所有拥有的流。不幸的是,流没有Detach()
方法,因此您必须在此处创建一些解决方法。Dispose()
on parent willDispose()
all owned streams. Unfortunately, streams don't haveDetach()
method, so you have to create some workaround here.我不知道为什么,但您可以不处理您的 StreamReader。这样,即使 StreamReader 被收集,您的底层流也不会被处理。
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.