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会以这种方式影响关联的流?期望一个可搜索的流可以被多次读取,包括几个StreamReaders,这难道不是合乎逻辑的吗?

4

6 回答 6

16

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

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

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

于 2010-10-10T18:43:46.897 回答
7

的目的Dispose()是在您完成流时清理资源。reader影响的原因是因为 reader 只是在过滤流,因此处理 reader 没有任何意义,除非在它链接到源流的调用的上下文中。

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

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)))
{
    ...
}
于 2010-10-10T18:12:53.513 回答
2

Using定义了一个范围,在该范围之外将放置一个对象,因此ObjectDisposedException. 您无法在此块之外访问 StreamReader 的内容。

于 2010-10-10T18:15:38.267 回答
1

我同意你的问题。这种有意的副作用的最大问题是当开发人员不知道它并且盲目地遵循将 StreamReader 与using. 但是当它位于长寿命对象的属性上时,它可能会导致一些非常难以追踪的错误,我见过的最好(最差?)的例子是

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

开发人员不知道 InputStream 现在已经为任何期望它存在的地方准备好了。

显然,一旦您了解了内部结构,您就可以避免using并且只需读取并重置位置即可。但我认为 API 设计的核心原则是避免副作用,尤其是不要破坏你正在操作的数据。一个被认为是“阅读器”的类所固有的任何东西都不应该在“使用”它时清除它读取的数据。处置读者应该释放对流的任何引用,而不是清除流本身。我唯一能想到的是,必须做出选择,因为读者正在改变 Stream 的其他内部状态,比如寻找指针的位置,他们假设如果你在它周围包裹一个 using,你就会故意去做完一切。另一方面,就像在您的示例中一样,如果您正在创建一个流,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);

(对不起,我添加了这个作为答案,不适合评论,我希望更多地讨论这个框架的设计决策)

于 2012-10-22T19:41:51.820 回答
0

Dispose()在父级将Dispose()所有拥有的流。不幸的是,流没有Detach()方法,所以你必须在这里创建一些解决方法。

于 2010-10-10T18:52:00.117 回答
0

我不知道为什么,但你可以让你的 StreamReader 闲置。这样,即使 StreamReader 被收集,您的基础流也不会被处理。

于 2010-10-10T19:08:12.473 回答