处理嵌套“using”语句时“Dispose”抛出的exception

显然,使用嵌套的using语句时,一些exception可能会丢失。 考虑这个简单的控制台应用

 using System; namespace ConsoleApplication { public class Throwing: IDisposable { int n; public Throwing(int n) { this.n = n; } public void Dispose() { var e = new ApplicationException(String.Format("Throwing({0})", this.n)); Console.WriteLine("Throw: {0}", e.Message); throw e; } } class Program { static void DoWork() { // ... using (var a = new Throwing(1)) { // ... using (var b = new Throwing(2)) { // ... using (var c = new Throwing(3)) { // ... } } } } static void Main(string[] args) { AppDomain.CurrentDomain.UnhandledException += (sender, e) => { // this doesn't get called Console.WriteLine("UnhandledException:", e.ExceptionObject.ToString()); }; try { DoWork(); } catch (Exception e) { // this handles Throwing(1) only Console.WriteLine("Handle: {0}", e.Message); } Console.ReadLine(); } } } 

Throwing每个实例在被处理时抛出。 永远不会调用AppDomain.CurrentDomain.UnhandledException

输出:

投掷:投掷(3)
投掷:投掷(2)
投掷:投掷(1)
处理:投掷(1)

我更喜欢至少能够记录丢失的Throwing(2)Throwing(3)我如何做到这一点,而不是为每个using单独的try/catch (这将有点杀死using的方便)?

在现实生活中,这些对象通常是我无法控制的类的实例。 他们可能会或可能不会投掷,但如果他们这样做,我希望可以选择观察这些例外情况。

当我在考虑降低嵌套using的级别时出现了这个问题。 有一个简洁的答案暗示汇总exception。 有趣的是,这与嵌套的using语句的标准行为有何不同。

[编辑]这个问题似乎密切相关: 你应该实现IDisposable.Dispose(),以便它永远不会抛出?

您注意到的是Disposeusing设计中的一个基本问题,目前还没有很好的解决方案。 恕我直言,最好的设计是拥有一个Dispose版本,它接收任何可能挂起的exception(如果没有挂起,则为null ),如果需要抛出一个exception,可以记录或封装该exception。 否则,如果您既可以控制在using中也可以在Dispose导致exception的代码,那么您可以使用某种外部数据通道让Dispose知道内部exception,但这是相当的做作。

太糟糕了,没有适当的语言支持与finally块相关联的代码(显式地或隐式地通过using )来知道相关的try是否正确完成,如果没有,则出现了什么问题。 Dispose应该默默地失败的概念是恕我直言,非常危险和错误的。 如果一个对象封装了一个打开写入的文件,并且Dispose关闭了该文件(一个常见的模式)并且无法写入数据,那么让Dispose调用正常返回会导致调用代码认为数据写得正确,可能允许它覆盖了唯一的好备份。 此外,如果文件应该显式关闭并且调用Dispose而不关闭文件应该被视为错误,这意味着如果受保护的块将正常完成,但是如果受保护的块无法调用Close ,则Dispose应该抛出exception因为首先发生exception,让Dispose抛出exception将是非常无益的。

如果性能不是很关键,你可以在VB.NET中编写一个包装器方法,它接受两个代理(类型为Action和一个Action ),在try块中调用第一个,然后在finally调用第二个块与try块中发生的exception(如果有)。 如果包装器方法是用VB.NET编写的,它可以发现并报告发生的exception, 不必捕获并重新抛出它。 其他模式也是可能的。 包装器的大多数用法都涉及闭包,这些闭包很笨,但包装器至少可以实现正确的语义。

另一种可以避免闭包的包装设计,但要求客户正确使用它并且几乎不提供防止错误使用的保护,这样会产生如下用法:

 var dispRes = new DisposeResult(); ... try { .. the following could be in some nested routine which took dispRes as a parameter using (dispWrap = new DisposeWrap(dispRes, ... other disposable resources) { ... } } catch (...) { } finally { } if (dispRes.Exception != null) ... handle cleanup failures here 

这种方法的问题在于没有办法确保任何人都会评估dispRes.Exception 。 可以使用终结器来记录在没有检查过的情况下放弃dispRes情况,但是没有办法区分发生这种情况的情况,因为exception将代码踢出if测试,或者因为程序员忘记了检查。

PS – Dispose确实应该知道是否发生exception的另一种情况是,当IDisposable对象用于包装锁或其他范围时,对象的不变量可能会暂时失效,但在代码离开范围之前需要恢复。 如果发生exception,代码通常不会期望解决exception,但是应该根据它进行操作 ,使锁既不保持也不释放,而是无效 ,以便任何现在或将来尝试获取它将引发exception。 如果将来没有尝试获取锁或其他资源,则它无效的事实不应该破坏系统操作。 如果资源对于程序的某些部分是非常必要的,那么使其无效将导致程序的该部分死亡,同时最小化它对其他任何事物造成的损害。 我知道用唯一的语义真正实现这个案例的唯一方法是使用icky闭包。 否则,唯一的选择是要求显式的invalidate / validate调用,并希望在资源无效的代码部分内的任何return语句之前都要调用validate。

有一个代码分析器警告。 CA1065 ,“不要在意外位置引发exception”。 Dispose()方法在该列表上。 “框架设计指南”第9.4.1章中的强烈警告:

避免在Dispose(bool)中抛出exception,除非在包含进程已被破坏的临界情况下(泄漏,不一致的共享状态等)。

这是错误的,因为using语句在finally块中调用Dispose()。 finally块中引发的exception可能会产生令人不快的副作用,如果在堆栈因exception而展开时调用finally块,则它会替换活动exception。 你到底发生了什么。

Repro代码:

 class Program { static void Main(string[] args) { try { try { throw new Exception("You won't see this"); } finally { throw new Exception("You'll see this"); } } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadLine(); } } 

也许一些辅助函数可以让你编写类似于using代码:

  void UsingAndLog(Func creator, Action action) where T:IDisposabe { T item = creator(); try { action(item); } finally { try { item.Dispose();} catch(Exception ex) { // Log/pick which one to throw. } } } UsingAndLog(() => new FileStream(...), item => { //code that you'd write inside using item.Write(...); }); 

请注意,我可能不会使用此路由,只是让Dispose中的exception从正常using代码中覆盖我的exception。 如果库从Dispose反对强烈建议不这样做,那么很有可能它不是唯一的问题,并且需要重新考虑这种库的有用性。