为什么释放StreamReader会使流变得不可读?

10

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

但是以下代码会抛出 ObjectDisposedException: 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读取多次吗?


1
还可以考虑在这种情况下使用System.IO.File.ReadAllText()。它更简单。 - Dave Markle
@Dave Markle:你说得对。我只是举了一个简短的例子。实际上,在真正的代码中,我处理的流可能非常大,因此第一个读取器逐行读取它们,然后将流每个字节复制到另一个流中。 - Arseni Mourzenko
6个回答

16
这是因为 StreamReader 接管了流的所有权。换句话说,它负责关闭源流。当你的程序调用 DisposeClose(在你的情况下离开 using 语句范围)时,它也会处理源流的释放,如在你的例子中调用 fs.Dispose()。因此,在第一个 using 块之后,文件流就已经无法使用了。所有 .NET 中包装另一个流的流类都遵循这种一致行为。 StreamReader 有一个构造函数可以指定它不拥有源流的所有权。但是在 .NET 程序中无法访问该构造函数,因为它是内部的。
在这种特殊情况下,您可以通过不使用 using 语句来解决问题。但这是一个相当复杂的实现细节。您可能有更好的解决方案,但代码过于简单,无法提出真正的建议。

7
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.Seek)。在这些情况下,StreamReader 链接其对底层流的调用,使您可以通过根本不使用流的 usings 语句来节省代码。例如,代码将如下所示:

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

实际上,我想知道为什么在 reader 上调用 Dispose() 会影响 stream。我不理解你的回答。这是否意味着读取器的 Dispose 只有清除流数据的作用?那么为什么 StreamReader 实现了 IDisposable,如果它不执行任何有用的操作(因为在所有情况下,处理流本身将完成所有工作)? - Arseni Mourzenko
2
@MainMa,Dispose() 的目的始终是确保清理与给定的 IDisposable 相关联的所有资源。由于读取器和流表示相同的实体,因此处理其中一个与处理另一个具有相同的效果。 - Kirk Woll

2

Using 定义了一个作用域,在该作用域之外,对象将被处理,因此会抛出 ObjectDisposedException 异常。你不能在这个块之外访问 StreamReader 的内容。


1

我同意你的观点。这种有意的副作用最大的问题在于,当开发人员不知道它的存在并盲目地遵循“最佳实践”将StreamReader包装在using中时。但是当它出现在长期存在的对象属性上时,它可能会导致一些非常难以追踪的错误,最糟糕的例子就是:

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

开发人员不知道 InputStream 现在已经无法用于任何未来需要它的地方。
显然,一旦你了解内部情况,你就知道要避免使用 using,而是直接读取并重置位置。但我认为 API 设计的核心原则之一是避免副作用,尤其是不破坏你正在处理的数据。一个被认为是“读取器”的类本质上不应该在完成“使用”后清除它所读取的数据。释放读取器应该释放对 Stream 的任何引用,而不是清除 Stream 本身。唯一能想到的事情是,由于读取器正在改变 Stream 的其他内部状态,比如寻址指针的位置,他们假设如果你在它周围包装了一个 using,那么你有意结束所有操作。另一方面,就像你的例子一样,如果你创建了一个 Stream,那么 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);

(抱歉我将其添加为答案,无法放在评论中,我希望能就框架的这个设计决策进行更多讨论)


2
一个更好的StreamReader设计应该是有一个构造函数参数来指定它是否应该拥有流的所有权。有许多情况下,代码可能会打开一个流,为其创建一个读取器,将读取器交给将来某个时间读取数据的东西,然后不再使用该流。在这种情况下,当读取器完成对流的操作时,应该处理该流,但是流的创建者可能不知道何时会发生这种情况。 - supercat

0
在父对象上调用 Dispose() 方法将会释放所有已拥有的流。不幸的是,流没有 Detach() 方法,所以你必须在这里创建一些解决方法。

0

我不知道为什么,但你可以让你的StreamReader未被处理。这样,即使StreamReader被收集,底层流也不会被处理。


当然。但是对我来说,不给 StreamReader 加上 using 不太直观,而且可能会违反 FxCop 规则。顺便说一下,这个问题的起源是在我重构旧代码时出现的,其中完全没有使用 using。 - Arseni Mourzenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接