11

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

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). 我该如何做到这一点,而无需try/catch为每个using单独使用(这会破坏 的便利性using)?

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

当我正在考虑降低嵌套的级别时,using这个问题出现了。有一个简洁的答案建议聚合异常。有趣的是,这与嵌套using语句的标准行为有何不同。

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

4

3 回答 3

23

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

避免在 Dispose(bool) 中引发异常,除非在包含进程已损坏(泄漏、不一致的共享状态等)的危急情况下。

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

复制代码:

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();
    }
}
于 2013-10-08T04:17:49.537 回答
3

也许一些帮助函数可以让你编写类似于以下的代码using

 void UsingAndLog<T>(Func<T> creator, Action<T> 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(...);
 });

请注意,我可能不会走这条路,只是让异常 fromDispose覆盖我在 normal 中的代码的异常using。如果图书馆Dispose反对强烈建议不要这样做,那么很有可能它不是唯一的问题,并且需要重新考虑此类图书馆的有用性。

于 2013-10-08T05:06:59.053 回答
3

您注意到的是Disposeand设计中的一个基本问题using,目前还没有很好的解决方案。恕我直言,最好的设计是拥有一个版本,Dispose该版本接收任何可能未决的异常作为参数(或者null,如果没有未决的异常),并且如果需要抛出自己的异常,则可以记录或封装该异常。using否则,如果您同时控制了可能在 内和 内导致异常的代码Dispose,您也许可以使用某种外部数据通道来让Dispose了解内部异常,但这有点做作。

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

如果性能不重要,您可以在 VB.NET 中编写一个包装器方法,该方法将接受两个委托(类型Action和一个Action<Exception>),在块中调用第一个try,然后在finally块中调用第二个,异常发生在try块(如果有的话)。如果包装器方法是用 VB.NET 编写的,它可以发现并报告发生的异常,而无需捕获并重新抛出它。其他模式也是可能的。包装器的大多数用法都涉及到闭包,这很糟糕,但包装器至少可以实现适当的语义。

另一种包装器设计可以避免闭包,但需要客户端正确使用它并且对不正确使用提供很少的保护,这样的使用击球手:

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未经检查就被放弃的情况,但是没有办法区分发生这种情况的情况,因为异常将代码踢出if测试之外,或者因为程序员只是忘记了检查。

PS——另一种Dispose真正应该知道是否发生异常的情况是,当IDisposable对象用于包装锁或其他范围时,对象的不变量可能会暂时失效,但预计会在代码离开范围之前恢复。如果发生异常,代码通常不应期望解决异常,但仍应根据异常采取行动,使锁既不被持有也不被释放,而是无效,因此任何现在或将来的获取它的尝试都会抛出异常。如果以后没有尝试获取锁或其他资源,则它无效的事实不应中断系统操作。如果该资源对程序的某些部分至关重要,则使其无效将导致程序的该部分死亡,同时最大限度地减少它对其他任何部分造成的损害。我知道用良好的语义真正实现这种情况的唯一方法是使用 icky 闭包。否则,唯一的替代方法是要求显式的 invalidate/validate 调用,并希望在资源无效的代码部分中的任何 return 语句都在调用 validate 之前。

于 2013-10-08T23:30:07.557 回答