需要在using块中包装StreamWriter吗?

26

几天前,我发布了如下代码:

StreamWriter writer = new StreamWriter(Response.OutputStream);
writer.WriteLine("col1,col2,col3");
writer.WriteLine("1,2,3");
writer.Close();
Response.End();

我被告知,如果出现异常,应该在StreamWriter周围加上using块。 这样的更改将使它看起来像这样:

using(StreamWriter writer = new StreamWriter(Response.OutputStream))
{
    writer.WriteLine("col1,col2,col3");
    writer.WriteLine("1,2,3");
    writer.Close(); //not necessary I think... end of using block should close writer
}
Response.End();

我不确定这个改变有什么价值。如果没有 using 块发生异常,写入器和响应仍然会被清理干净,对吧?那 using 块能给我带来什么呢?

10个回答

23

在第一个例子中,流将保持打开状态,因为错误将否定关闭它。

using运算符强制调用Dispose()方法,该方法应在退出块时清理对象并关闭所有打开的连接。


17

我将提出不同意见。对于特定问题“是否有必要将StreamWriter包装在using块中?”,答案实际上是不需要。事实上,你不应该调用StreamWriter的Dispose方法,因为它的设计存在问题并会导致错误的结果。

StreamWriter的问题在于,在你Dispose它时,它也会Dispose底层流。如果你使用文件名创建StreamWriter,并且它内部创建了自己的FileStream,则此行为完全合适。但是,如果像这里一样,你使用现有的流创建StreamWriter,则这种行为绝对是错误的。但是StreamWriter仍会执行这种行为。

像下面这样的代码将无法正常工作:

var stream = new MemoryStream();
using (var writer = new StreamWriter(stream)) { ... }
stream.Position = 0;
using (var reader = new StreamReader(stream)) { ... }
由于当 StreamWriter 的 using 块释放掉 StreamWriter 时,也会一并丢弃流。所以当您尝试从该流中读取时,就会出现 ObjectDisposedException 异常。
StreamWriter 违反了“自己收拾自己的垃圾”的原则。它试图清理其他人的垃圾,无论他们是否想要这样做。
(想象一下如果您在现实生活中这样做。试着向警察解释为什么您闯入别人家里并开始把所有东西扔进垃圾桶...)
因此,我认为 StreamWriter(和做同样事情的 StreamReader)是极少数几个“如果实现了 IDisposable,则应调用 Dispose”是错误的类之一。不要在已创建基于现有流的 StreamWriter 上调用 Dispose。而是应该调用 Flush()。
然后,只需确保在应该清理流时清理即可。(如 Joe 指出的,ASP.NET 会为您处理 Response.OutputStream 的处理,因此您不需要在此处担心。)
注意:如果您没有对 StreamWriter 进行 Dispose,则在完成写入操作时需要调用 Flush()。否则,可能仍然存在在内存中缓冲的数据,但它永远不会传输到输出流中。
我的 StreamReader 规则是,假装它没有实现 IDisposable。只需在完成时将其放开即可。
我的 StreamWriter 规则是,在您本应调用 Dispose 的地方调用 Flush。(这意味着您必须使用 try..finally 代替 using。)

我的 StreamReader 原则是,假装它没有实现 IDisposable 接口,但如果你使用文件名构造了 StreamReader,则该对象拥有流,因此不应忽略 IDisposable 接口。我不同意 StreamReader/Writer 设计得很糟糕这一观点。虽然我理解您所描述的问题,但所有的设计都是权衡取舍,我认为在大多数情况下其行为是正确的。当然,在上面的示例中,你现在必须嵌套使用 using 语句,但这并不是什么大问题。关于处理资源释放的责任问题,这里有一些讨论:https://dev59.com/iXRB5IYBdhLWcg3wQVWV - Joe
@Joe:你的反规则太强了。应该是,不要调用Dispose(因此不要实现using块)在StreamReader / StreamWriter上,除非你打算关闭底层流。也许需要像XmlReaderSettings上的CloseInput标志一样的东西,但是针对StreamReader。 - John Saunders
@Joe:没错。他们本应该这样做,如果你使用文件名构造它,那么构造函数会创建一个流并在Dispose中关闭它;但是如果你使用流来构造它,它不会关闭它,因为有其他人拥有它。 - Joe White
1
@John:所以你喜欢晦涩难懂的代码?(微笑)别这样。如果你想关闭底层流,则关闭底层流。让你的代码表达你的意思。 - Joe White
13
.Net 4.5修复了这一疏漏,现在允许您选择性地保持流的打开状态:http://msdn.microsoft.com/en-us/library/gg712853.aspx - CubanX

3

在使用StreamWriter时,将其包装在using块中与以下代码基本等效:

StreamWriter writer;
try
{
    writer = new StreamWriter(Response.OutputStream);
    writer.WriteLine("col1,col2,col3");
    writer.WriteLine("1,2,3");
}
catch
{
    throw;
}
finally
{
    if (writer != null)
    {
        writer.Close();    
    }
}

虽然你完全可以自己编写此代码,但将其放入using块中要简单得多。


1
关闭StreamWriter将同时关闭Response.OutputStream。如果您发现数据未传输到客户端,请注意此警告。 - Matthew Whited
1
只需省略catch() {}部分,它没有任何意义,只会让人感到困惑。 - H H

3

如果没有使用using块就发生异常并终止程序,你将留下未关闭的连接。使用using块将始终为您关闭连接,类似于使用try{}catch{}finally{}。


1
如果程序退出,句柄将被清理。using语句仅在应用程序运行时有帮助。 - Paul Alexander
Finalizers 是指在退出时进行清理的术语。 - Guvante

3

最终,写入器将被清理。这是由垃圾回收器决定的,它会注意到命令的Dispose方法没有被调用,并调用它。当然,根据情况,垃圾回收器可能需要几分钟、几小时或几天才能运行。如果写入器正在独占某个文件的锁定状态,那么即使您已经完成了操作,其他进程也无法打开该文件。

using块确保始终调用Dispose方法,因此无论发生什么控制流,都会调用Close方法。


通常,對於成員參考未管理的資源,相同的清理代碼將從兩者中調用。我上面說過“Invoke it”,但不能確定Dispose函數是否被調用。不過,在這兩種情況下,行為可能會非常類似。 - Adam Wright
是的。GC 运行并检测所有没有根的对象。任何具有 finalizer 方法的对象都不会被收集,而是排队等待 finalizer 线程启动。finalizer 在排队的对象上运行 dispose 方法。(这就是为什么您应该在自己的对象上遵循正确的 dispose 模式 - 确保在从 finalizer 或 using 块中调用时执行正确的操作)。这也是为什么使用 using 很重要,因为具有 finalizer 的对象将在运行 finalizer 时提升到下一个 GC 代,因此如果未正确处理,则会存活更长时间。 - Simon P Stevens
@Adam 谢谢你的解释。昨天我发布了一个(可能措辞不当)关于这个领域的问题。很多人似乎固执地重复解释如何使用翻译成try/catch/finally,这并没有什么帮助 :)@Luke 我的意思并不是暗示它会自动调用。只是程序员会手动添加dispose/cleanup调用到终结器中。 - xyz
@frou:我的评论是针对Adam的,不是你。Adam的回答暗示了GC会自动调用Dispose方法。事实并非如此。 - LukeH
这个答案并不完全准确。其他代码可能会有一个句柄来处理流并释放它(在这种情况下就是这样 -- 流由 ASP.NET 拥有,它可能会释放它,而不是等待垃圾回收)。 - Ben Voigt
显示剩余5条评论

1
在我看来,将实现IDisposable接口的任何类都包装在using块中是必要的。实现IDisposable接口意味着该类具有需要清理的资源。

1
如果您需要长时间维持该打开状态,例如异步套接字和流,那该怎么办呢?您可以自行实现IDisposable的原因。此外,IDisposable也会关闭流。这意味着,如果您像上面那样重定向内容(例如,从StreamWriter),并且处置了StreamWriter,则StreamWriter将关闭传递给它的流。这可能会防止数据发送到客户端。 - Matthew Whited

1

我的经验法则是,如果我在智能感知中看到Dispose被列出,我就会将其包装在using块中。


好的规则,但不够。一些类使用显式实现IDisposable,有效地隐藏Dispose()。这是一个坏主意,但它正在被使用。你仍然需要应用using(){}。 - H H

0

using 块在结束时调用 dispose()。这只是一种方便的方式,确保资源及时清理。


常见的误解是,Dispose 是一种告诉类的使用者对象应该被清理的方式,而 Finalizers 则是垃圾回收器在对象离开作用域或程序结束时进行清理的方式。 - Guvante
使用此方法关闭或释放由实现此接口的类的实例所持有的非托管资源,例如文件、流和句柄。按照惯例,此方法用于与释放对象持有的资源或准备对象以便重用相关的所有任务。 - Robert

0
在几乎所有的情况下,如果一个类实现了IDisposable接口,并且如果你正在创建该类的实例,那么你需要使用using块。

@John -- 有任何异常吗? - Jamie Ide
据我所知,只有WCF客户端代理。这是由于微软的设计错误导致的。 - John Saunders
@John:你怎么判断这是一个错误?有时候你需要保持输出流开放,只有在客户端完成后才关闭它。如果在客户端下载所有数据之前就关闭它,连接将被中止,客户端将永远无法接收数据。 - Matthew Whited
@Matthew:设计错误在于当您在WCF代理上使用using块时,它会中断。如果块中有未处理的异常,将调用Dispose,该方法又会调用Abort,可能会引发自己独立的异常。我会失去有关原始问题的所有信息。 - John Saunders
我认为StreamReader和StreamWriter是这个规则的例外。请看我的回答。 - Joe White

0

虽然始终处理可释放类(如StreamWriter)是良好的实践,但在这种情况下并不重要。

当ASP.NET基础结构完成处理您的请求时,Response.OutputStream将被处理。

StreamWriter假定它“拥有”传递给构造函数的流,并且因此在处理时会关闭该流。 但在您提供的示例中,流是在代码外部实例化的,因此将有另一个所有者负责清理。


StreamWriter怎么办?如果它除了包装的流之外还持有一些未受管理的资源呢? - John Saunders
它没有任何其他未受管控的资源。但我同意将其释放是个好习惯,我只是在指出在这种情况下,流无论如何都将被释放。 - Joe
如果您不处置 StreamWriter,则在完成时需要调用 Flush()。否则,您的输出的一部分可能仍然被缓存在内存中,并且尚未写入流。 - Joe White
@Joe White: "如果你不释放StreamWriter,你确实需要调用Flush()" - 一般情况下是这样的。我只是在说,在这个特定的情况下,释放(因此刷新)由ASP.NET基础设施处理。 - Joe

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