我需要从开始到结束两次读取一个流。
但是以下代码会引发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
s?
答案 0 :(得分:13)
这是因为StreamReader
接管了流的“所有权”。换句话说,它使自己负责关闭源流。只要您的程序调用{{1}}或Dispose
(在您的情况下保留Close
语句范围),它就会处理源流。在你的情况下调用using
。因此,在离开第一个fs.Dispose()
块后文件流已经死了。这是一致行为,.NET中包含另一个流的所有流类都以这种方式运行。
using
有一个构造函数允许说不拥有源流。但是,无法从.NET程序访问它,构造函数是内部的。
在这种特殊情况下,您可以通过不使用StreamReader
的{{1}}语句来解决问题。然而,这是一个相当毛茸茸的实现细节。肯定有更好的解决方案可供您使用,但代码太合成了,无法提出真正的解决方案。
答案 1 :(得分:7)
Dispose()
的目的是在完成流后清理资源。 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)))
{
...
}
答案 2 :(得分:2)
Using
定义了一个范围,在该范围之外将放置一个对象,即ObjectDisposedException
。您无法访问此块之外的StreamReader内容。
答案 3 :(得分:1)
我同意你的问题。这种故意副作用的最大问题是开发人员不了解它并且盲目地遵循围绕使用using
的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);
(抱歉,我添加了这个作为答案,不适合评论,我希望更多关于框架设计决策的讨论)
答案 4 :(得分:0)
Dispose()
将Dispose()
所有拥有的流。不幸的是,流没有Detach()
方法,所以你必须在这里创建一些解决方法。
答案 5 :(得分:0)
我不知道为什么,但你可以不让你的StreamReader离开。这样,即使收集StreamReader,也不会处理您的基础流。