为什么要部署StreamReader会使流不可读?

我需要从开始到结束两次读取一个流。

但是下面的代码抛出了ObjectDisposedException: Cannot access a closed fileexception。

 string fileToReadPath = @""; 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

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

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

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

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使用usings语句来节省代码。 例如,代码看起来像:

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

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

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

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

开发人员不知道InputStream现在已经被任何未来的地方用于预期。

显然,一旦你知道内部,你知道要避免using ,只需阅读和重置位置。 但我认为API设计的核心原则是避免副作用,特别是不要破坏你正在处理的数据。 一个被认为是“读者”的类没有什么固有的东西可以清除它在“使用”它时所读取的数据。 处理读者应该释放对Stream的任何引用,而不是清除流本身。 我唯一能想到的是必须做出选择,因为读者正在改变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); 

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

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

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