处理“释放”引发的异常,同时展开嵌套的“using”语句

本文关键字:using 语句 嵌套 异常 释放 处理 | 更新日期: 2023-09-27 18:34:31

显然,使用嵌套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).我该如何做到这一点,而不为每个using诉诸单独的try/catch (这会有点扼杀using的便利性(?

在现实生活中,这些对象通常是我无法控制的类的实例。他们可能会也可能不会抛出,但如果他们这样做,我希望有一个选项来观察这些例外。

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

[编辑] 这个问题似乎密切相关:你应该实现IDisposable.Dispose((这样它就不会抛出吗?

处理“释放”引发的异常,同时展开嵌套的“using”语句

有一个代码分析器警告。 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();
    }
}

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

太糟糕了,对于与finally块关联的代码没有适当的语言支持(显式或通过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对象用于包装锁或其他作用域时,对象的不变量可能暂时失效,但希望在代码离开作用域之前恢复。 如果发生异常,代码通常不应期望解决异常,但仍应基于它采取措施,使锁既不保留也不释放,而是无效,因此任何当前或将来获取它的尝试都将引发异常。 如果将来没有尝试获取锁或其他资源,则它无效的事实不应中断系统操作。 如果资源对程序的某些部分至关重要,则使其无效将导致程序的该部分死亡,同时最大限度地减少它对其他任何部分造成的损害。 我知道要真正用良好的语义实现这种情况的唯一方法是使用令人讨厌的闭包。 否则,唯一的替代方法是要求显式失效/验证调用,并希望在资源无效的代码部分中的任何 return 语句之前都带有验证调用。

也许一些辅助函数可以让你编写类似于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(...);
 });

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