为什么处置 StreamReader 会使流变得不可读?

发布于 2024-09-27 05:01:50 字数 728 浏览 3 评论 0原文

我需要从头到尾读取流两次。

但以下代码会引发 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 StreamReaders?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

墨小墨 2024-10-04 05:01:50

发生这种情况是因为 StreamReader 接管了流的“所有权”。换句话说,它让自己负责关闭源流。一旦您的程序调用 DisposeClose (在您的情况下保留 using 语句范围),它也会处理源流。在您的情况下调用 fs.Dispose() 。因此,文件流在离开第一个 using 块后就死了。这是一致的行为,.NET 中包装另一个流的所有流类都以这种方式运行。

StreamReader 有一个构造函数,允许声明它拥有源流。然而,它不能从 .NET 程序访问,构造函数是内部的。

在这种特殊情况下,您可以通过不使用 StreamReaderusing 语句来解决该问题。然而,这是一个相当棘手的实现细节。肯定有更好的解决方案可供您使用,但代码过于综合,无法提出真正的解决方案。

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.

是你 2024-10-04 05:01:50

Dispose() 的目的是在完成流后清理资源。 读取器影响的原因是读取器只是过滤流,因此处理读取器没有任何意义,除非在它将调用链接到源流也是如此。

要修复您的代码,只需始终使用一个阅读器:

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

编辑以解决下面的注释

在大多数情况下,您不需要像在代码中那样访问底层流 (fs .寻求)。在这些情况下,StreamReader 将其调用链接到底层流,这一事实使您可以通过根本不使用流的 usings 语句来节省代码。例如,代码如下所示:

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

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)))
{
    ...
}
昔日梦未散 2024-10-04 05:01:50

Using 定义了一个范围,在该范围之外的对象将被释放,因此会出现 ObjectDisposeException。您无法访问此块之外的 StreamReader 内容。

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.

や三分注定 2024-10-04 05:01:50

我同意你的问题。这种故意的副作用的最大问题是,开发人员不知道这一点,并且盲目地遵循使用 using 围绕 StreamReader 的“最佳实践”。但是,当它位于长期存在的对象的属性上时,它可能会导致一些非常难以追踪的错误,我见过的最好(最差?)的例子是

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

开发人员不知道InputStream现在已被用于任何未来期望的地方它在那里。

显然,一旦您了解了内部原理,您就知道要避免 using 并仅读取并重置位置。但我认为 API 设计的核心原则是避免副作用,尤其是不要破坏您正在操作的数据。一个被认为是“读取器”的类在“使用”完成后不应清除它读取的数据。处理 reader 应该释放对 Stream 的所有引用,而不是清除流本身。我唯一能想到的是,必须做出选择,因为读者正在改变流的其他内部状态,比如搜索指针的位置,他们假设如果你在它周围包装一个 using ,你会故意去一切都办完了。另一方面,就像在您的示例中一样,如果您正在创建一个 Stream,则该流本身将位于 using 中,但如果您正在读取在直接方法之外创建的 Stream,代码清除数据是冒昧的。

我所做的并告诉我们的开发人员在读取代码未显式创建的 Stream 实例上执行的操作是...

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

(抱歉,我将其添加为答案,不适合评论,我希望更多地讨论此设计框架的决定)

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)

染火枫林 2024-10-04 05:01:50

父级上的 Dispose()Dispose() 所有拥有的流。不幸的是,流没有 Detach() 方法,因此您必须在此处创建一些解决方法。

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

仙女 2024-10-04 05:01:50

我不知道为什么,但您可以不处理您的 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文