处理嵌套"using"语句时"Dispose"抛出的异常

nos*_*tio 11 c#

显然,使用嵌套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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

处理时每个Throwing抛出的实例.AppDomain.CurrentDomain.UnhandledException永远不会被召唤.

输出:

Throw: Throwing(3)
Throw: Throwing(2)
Throw: Throwing(1)
Handle: Throwing(1)

我更愿意至少能够记录缺失Throwing(2)Throwing(3).我如何做到这一点,而不是try/catch为每个人分别采取单独的措施using(这会扼杀方便using)?

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

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

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

Han*_*ant 23

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

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

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 嗯,不,你*总是*得到例外.它不是你所希望的那个并不能消除这两个事情在程序中出现严重错误的事实.避免挑选最喜欢的,垃圾+废话仍然是垃圾. (7认同)

sup*_*cat 3

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

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

如果性能并不重要,您可以在 VB.NET 中编写一个包装器方法,该方法将接受两个委托(类型ActionAction<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
Run Code Online (Sandbox Code Playgroud)

这种方法的问题在于,无法确保任何人都会评估dispRes.Exception. 人们可以使用终结器来记录dispRes未经检查而被放弃的情况,但是无法区分发生这种情况的情况,因为异常将代码踢出测试之外if,或者因为程序员只是忘记了检查。

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