【问题标题】:Handling exceptions thrown by "Dispose" while unwinding nested "using" statements在展开嵌套的“using”语句时处理“Dispose”引发的异常
【发布时间】:2013-10-14 20:01:05
【问题描述】:

显然,在使用嵌套的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 的便利性)?

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

这个问题是在我查看reducing the level of nested using 时出现的。有一个neat answer 建议聚合异常。有趣的是,这与嵌套 using 语句的标准行为有何不同。

[EDITED] 这个问题似乎密切相关: Should you implement IDisposable.Dispose() so that it never throws?

【问题讨论】:

  • 我不认为有可能改变你的 Dispose 所以它@98​​7654324@,是吗?我绝对明白它为什么会发生,而且我不一定会称它们为“丢失”(我认为它类似于捕获异常,然后在 catch 块中抛出一个新异常)。编辑:也许我应该问,你必须用你的AppDomain.CurrentDomain.UnhandledException 处理程序来捕获它们,还是可以接受另一种机制?
  • 因此,反编译表明leave.s 位于try 语句生成的try 块的末尾。文档指出,此运算符“无条件”将控制权转移到在执行 finally 块之后指定的标签。因此,看起来异常确实“丢失”了..因为在 finally 块执行后控制是“无条件地”转移的。 (控制转移到 finally 块之后)。
  • @Noseratio: UnhandledException 没有抓住它们,因为它们没有被处理。相反,它们会被更间接的方式吞噬。
  • @Noseratio:您愿意使用托管一次性包装器(如LoggedDisposable)来包装一次性用品吗?您可能会像 using(var loggedDisposable = new LoggedDisposable(() => new Throwing(1)) { var myThrowing = loggedDisposable.WrappedDisposable; ... } 一样使用它,其中您的 LoggedDisposable.Dispose 方法有一个 try/catch 日志块,它包装了一个 WrappedDisposable.Dispose 调用?
  • (我把上面的评论搞砸了,我会在这里修复它的重要部分以供后代使用,因为它已经过了编辑窗口并且评论是非常不正确的)我的方式看到它,当它处理c 时,它会抛出一个异常。这会导致它离开 b 的 using 块,从而调用 b.Dispose。这又会引发一个新异常,导致它离开 a 的 using 块,从而调用 a.Dispose 引发自己的新异常...

标签: c#


【解决方案1】:

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

【讨论】:

  • Hans:这是因为我在上面的 cmets 中怀疑的 leave.s 操作数吗?
  • CLR 执行此操作。它必须在岩石和坚硬的地方之间进行选择,只能传播这些例外中的一个。它捡起石头。该选择与您在 catch 块抛出时看到的异常有些一致,最后一个很重要。
  • 我会澄清一下:我可能甚至不知道Dispose 在第 3 方库中引发了异常(甚至在我同事的代码中),因为它像这样被吞噬(除非我在调试器中启用第一次机会异常)。我不喜欢这个;我宁愿在上面的 cmets 中使用 @ChrisSinclair 的 LoggedDisposable 之类的包装器,或者来自 hereDisposableList,它聚合了此类异常。
  • 嗯,不,你总是得到一个例外。它不是您所希望的,但这并不能消除程序中两件事情严重错误的事实。避免选择最喜欢的,废话+废话仍然是废话。
  • @HansPassant:一些例外代表了“严重”错误的事情。其他代表以预期方式失败的事物。如果Dispose 中出现严重错误(例如,尝试写入文件失败),那通常将比受保护块中发生的任何事情更重要,但该异常中包含的信息将往往信息量少。正确的做法是抛出一个 DisposeFailureException 来封装所有其他内容,但不幸的是,这很难做到。
【解决方案2】:

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

【讨论】:

  • 我认为,这是处理异常最灵活的方式,当它作为using 模式的一部分被自动调用时,可能会在Dispose 中抛出。
  • @Noseratio:我对这种方法的最大抱怨是,即使在 try 中执行了 return 语句,编程语言也无法确保调用检查清理失败的代码堵塞。 try/finally 将导致代码在这种情况下被执行,但如果发生异常(这是想要的),但即使有返回也不会执行其他构造.
【解决方案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(...);
 });

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

【讨论】:

  • 如果一个封装文件或流的类缓存数据并写入Dispose,如果文件无法写入,应该Dispose方法做什么?如果应用程序调用了Close 并且没有挂起的异常,有某种方法可以抛出Dispose,这可能是最明智的方法,但不存在标准模式可做那干净。我不确定投掷Dispose 是否可以称为“缺陷”,除非有更好的替代方案。
  • @supercat 如果 using 块中有异常,则缓冲区可能不包含您的想法,并且您的文件无论如何都会损坏。为什么要尝试编写损坏的文件?另一方面,如果应用程序无一例外地关闭了文件(即这是您想要发生的事情),那么为什么 dispose 必须抛出?这听起来对我来说很明智......
  • @KrisVandermotten:我的意思是“如果应用程序没有调用Close,则抛出。”至于“无论如何文件都已损坏”,这几乎不是给定的。通常,如果数据采集和记录程序在获取数据时抛出异常,则未提供给异常的记录代码的数据将不可用,但记录代码应尝试完成写出所有可用的内容。至于Dispose throwing 如果“正常”执行,我们的想法是,如果在没有Close 的情况下调用Dispose 的唯一时间是当存在未决异常时,那么Dispose 可以计算...
  • ...即使它抑制了写入文件时发生的异常,应用程序也会知道“某事”是错误的(即任何异常导致Close 不发生)。但是,如果程序员只是忘记在没有Dispose 的情况下调用Close,那么这种机制将不起作用。如果发生这种情况,该类应该做一些事情来“设置警报”,让程序员知道问题需要解决。
  • @supercat “设置警报”可以通过实现终结器轻松完成。但要知道这会严重影响垃圾收集。
猜你喜欢
  • 2010-10-05
  • 2014-04-04
  • 2016-10-24
  • 2011-03-21
  • 1970-01-01
  • 1970-01-01
  • 2018-11-18
  • 1970-01-01
  • 2016-09-21
相关资源
最近更新 更多