对于C(析构函数)中的等效机制,建议是it should usually not throw any exceptions . 这主要是因为这样做可能会终止您的流程,这很少是一个好策略 .
在.NET的等效场景中......
-
抛出第一个异常
-
由于第一个异常,执行finally块
-
finally块调用Dispose()方法
-
Dispose()方法抛出第二个异常
...您的流程不会立即终止 . 但是,由于.NET无法用第二个异常替换第一个异常,因此会丢失信息 . 因此,调用堆栈上某处的catch块将永远不会出现第一个异常 . 然而,人们通常对第一个例外更感兴趣,因为这通常会提供更好的线索,说明为什么事情开始出错 .
由于.NET缺少一种机制来检测代码是否在异常处于挂起状态时被执行,因此似乎只有两种选择可以实现IDisposable:
-
始终吞下Dispose()中发生的所有异常 . 不好,因为你可能最终吞下OutOfMemoryException,ExecutionEngineException等等,我通常宁愿在它们发生时拆除它而没有另外的异常已经挂起 .
-
让所有异常传播出Dispose() . 不好,因为您可能会丢失有关问题根本原因的信息,请参阅上文 .
那么,两个邪恶中哪一个较小?有没有更好的办法?
EDIT :为了澄清,我在讨论让Dispose()调用的方法抛出的异常从Dispose()传播出来,例如:
using System;
using System.Net.Sockets;
public sealed class NntpClient : IDisposable
{
private TcpClient tcpClient;
public NntpClient(string hostname, int port)
{
this.tcpClient = new TcpClient(hostname, port);
}
public void Dispose()
{
// Should we implement like this or leave away the try-catch?
try
{
this.tcpClient.Close(); // Let's assume that this might throw
}
catch
{
}
}
}
8 回答
我认为吞咽是这种情况下两种邪恶中较小的一种,因为提高原始状态更好 - 警告: unless ,也许未能干净地处置它本身就非常关键(也许如果
TransactionScope
无法处理,因为这可能表示回滚失败) .有关此问题的更多想法,请参阅here - 包括包装/扩展方法的想法:
当然,你也可以做一些奇怪的事情,你用原始和第二个(
Dispose()
)异常重新抛出一个复合异常 - 但是想一想:你可能有多个using
块......它很快就会变得无法管理 . 实际上,最初的例外是有趣的 .Framework Design Guidelines(第2版)将此作为(§9.4.1):
评论[编辑]:
有指导方针,而不是硬性规则 . 这是一个"AVOID"而不是"DO NOT"指南 . 如上所述(在评论中)框架打破了这个(和其他)指导方针 . 诀窍是知道何时打破指南 . 在许多方面,这是一个熟练工和大师之间的区别 .
如果清理的某些部分可能失败,那么应该提供一个Close方法,该方法将抛出异常,以便调用者可以处理它们 .
如果您正在遵循dispose模式(如果类型直接包含某些非托管资源,则应该是这样),那么可以从终结器中调用
Dispose(bool)
,从终结器中抛出是一个坏主意,并且会阻止其他对象被最终确定 .我的观点:从Dispose中逃避的异常应该只是那些,如在指南中那样,充分的灾难性,以至于当前的过程不可能有进一步的可靠功能 .
Dispose
应该设计用于实现其目的,处理对象 . 这个任务 is safe and does not throw exceptions most of the time . 如果你发现自己从Dispose
抛出异常,你可能应该三思而后行,看看你是否做了太多的东西 . 除此之外,我认为Dispose
应该像所有其他方法一样对待:处理如果你可以用它做某事,如果你做不到就让它冒泡 .编辑:对于指定的示例,我会编写代码,以便我的代码不会导致异常,但清除
TcpClient
可能会导致异常,这应该在我看来有效传播(或处理和重新抛出更多一般异常,就像任何方法一样):但是,就像任何方法一样,如果你知道
tcpClient.Close()
可能抛出一个应该被忽略的异常(无关紧要)或者应该被另一个异常对象表示,那么你可能想 grab 它 .释放资源应该是一种“安全”操作 - 毕竟如何从无法释放资源中恢复?所以从Dispose抛出异常是没有意义的 .
但是,如果我在Dispose中发现程序状态已损坏,最好抛出异常然后吞下它,最好现在粉碎然后继续运行并产生不正确的结果 .
太糟糕了,微软没有为Dispose提供Exception参数,意图将它包装为InnerException,以防处理本身抛出异常 . 可以肯定的是,有效使用这样的参数需要使用C#不支持的异常过滤器块,但是这样的参数的存在是否可能促使C#设计者提供这样的功能?我希望看到的一个不错的变化是向Finally块添加Exception“参数”,例如
这将表现得像一个普通的最后一个块,除了'ex'将为null / Nothing如果'Try'运行完成,或者如果没有则保持抛出的异常 . 太糟糕了,没有办法让现有的代码使用这样的功能 .
我可能会使用日志记录来捕获有关第一个异常的详细信息,然后允许引发第二个异常 .
有各种策略可以从
Dispose
方法传播或吞咽异常,可能是基于是否还从主逻辑抛出了无法处理的异常 . 最佳解决方案是将决策权交给调用者,具体取决于他们的具体要求 . 我已经实现了这样做的通用扩展方法,提供:传播
Dispose
异常的默认using
语义Marc Gravell's suggestion总是在吞咽
Dispose
例外maxyfc's alternative只有当主逻辑出现异常时才会吞下
Dispose
异常将多个例外包装成
AggregateException
的Daniel Chambers's approach
类似的方法始终将所有异常包装到
AggregateException
(如Task.Wait
)这是我的扩展方法:
这些是实施的策略:
样品用途:
Update :如果需要支持返回值和/或异步的委托,则可以使用这些重载:
这是一种相当干净地抓取
using
或Dispose
内容抛出的任何异常的方法 .原始代码:
然后这里是抛出的代码,如果
codeInUsing()
抛出或foo.Dispose()
抛出或两者抛出,并让你看到第一个异常(有时包装为InnerExeption,取决于):这不是很好,但也不是太糟糕 .
这是实现它的代码 . 我设置它只是在没有附加调试器的情况下工作,因为当附加调试器时,我更担心它会在第一个异常的正确位置中断 . 您可以根据需要进行修改 .