【问题标题】:Intercepting an exception inside IDisposable.Dispose在 IDisposable.Dispose 中拦截异常
【发布时间】:2010-09-18 05:26:41
【问题描述】:

IDisposable.Dispose 方法中是否有办法判断是否抛出异常?

using (MyWrapper wrapper = new MyWrapper())
{
    throw new Exception("Bad error.");
}

如果using 语句中抛出异常,我想知道在处置IDisposable 对象时。

【问题讨论】:

  • 为什么你的实现 IDisposable 的包装器需要知道为什么要处理它?
  • 我想写一条日志消息,想知道语句里面的代码是运行成功还是中途因为异常而爆发了。
  • 看看这个来自ayende ayende.com/Blog/archive/2007/06/20/…

标签: c# .net idisposable


【解决方案1】:

现在,在 2017 年,这是执行此操作的通用方法,包括处理异常回滚。

    public static T WithinTransaction<T>(this IDbConnection cnn, Func<IDbTransaction, T> fn)
    {
        cnn.Open();
        using (var transaction = cnn.BeginTransaction())
        {
            try
            {
                T res = fn(transaction);
                transaction.Commit();
                return res;
            }
            catch (Exception)
            {
                transaction.Rollback();
                throw;
            }
            finally
            {
                cnn.Close();
            }
        }
    }

你这样称呼它:

        cnn.WithinTransaction(
            transaction =>
            {
                var affected = ..sqlcalls..(cnn, ...,  transaction);
                return affected;
            });

【讨论】:

    【解决方案2】:

    就我而言,我想在微服务崩溃时进行记录。我已经有一个 using 可以在实例关闭之前正确清理,但如果这是因为异常,我想知道原因,我讨厌回答不。

    与其试图让它在Dispose() 中工作,不如为你需要做的工作创建一个委托,然后将你的异常捕获包装在那里。所以在我的 MyWrapper 记录器中,我添加了一个采用 Action / Func 的方法:

     public void Start(Action<string, string, string> behavior)
         try{
            var string1 = "my queue message";
            var string2 = "some string message";
            var string3 = "some other string yet;"
            behaviour(string1, string2, string3);
         }
         catch(Exception e){
           Console.WriteLine(string.Format("Oops: {0}", e.Message))
         }
     }
    

    实施:

    using (var wrapper = new MyWrapper())
      {
           wrapper.Start((string1, string2, string3) => 
           {
              Console.WriteLine(string1);
              Console.WriteLine(string2);
              Console.WriteLine(string3);
           }
      }
    

    根据您需要做什么,这可能过于严格,但它可以满足我的需要。

    【讨论】:

      【解决方案3】:

      您可以使用方法Complete 扩展IDisposable 并使用这样的模式:

      using (MyWrapper wrapper = new MyWrapper())
      {
          throw new Exception("Bad error.");
          wrapper.Complete();
      }
      

      如果在using 语句中抛出异常,Complete 将不会在Dispose 之前调用。

      如果您想知道究竟抛出了什么异常,请订阅AppDomain.CurrentDomain.FirstChanceException 事件并将上次抛出的异常存储在ThreadLocal&lt;Exception&gt; 变量中。

      这种模式在TransactionScope 类中实现。

      【讨论】:

      • 是的,这是非常好的一点:AppDomain.CurrentDomain.FirstChanceException 以及一些 ThreadStatic 魔法和可能的类似 using(ExceptionTracker.Begin()) { if(ExceptionTracker.ThrownExceptions.Any()) { 。 . } } 神奇之处在于,该上下文中的任何代码都应该可以通过 ThrownExceptions 访问抛出的异常,并且它们不需要传递任何参数,关键是如果打开异常跟踪上下文,这些异常将可用于内部代码并且一旦该上下文关闭,就可以取消跟踪并清除异常列表
      【解决方案4】:

      不可能Dispose() 方法中捕获异常。

      但是,可以在 Dispose 中检查 Marshal.GetExceptionCode() 以检测是否确实发生了异常,但我不会依赖它。

      如果您不需要类并且只想捕获异常,您可以创建一个函数来接受在 try/catch 块中执行的 lambda,如下所示:

      HandleException(() => {
          throw new Exception("Bad error.");
      });
      
      public static void HandleException(Action code)
      {
          try
          {
              if (code != null)
                  code.Invoke();
          }
          catch
          {
              Console.WriteLine("Error handling");
              throw;
          }
      }
      

      例如,您可以使用自动执行事务的 Commit() 或 Rollback() 并执行一些日志记录的方法。 这样,您并不总是需要 try/catch 块。

      public static int? GetFerrariId()
      {
          using (var connection = new SqlConnection("..."))
          {
              connection.Open();
              using (var transaction = connection.BeginTransaction())
              {
                  return HandleTranaction(transaction, () =>
                  {
                      using (var command = connection.CreateCommand())
                      {
                          command.Transaction = transaction;
                          command.CommandText = "SELECT CarID FROM Cars WHERE Brand = 'Ferrari'";
                          return (int?)command.ExecuteScalar();
                      }
                  });
              }
          }
      }
      
      public static T HandleTranaction<T>(IDbTransaction transaction, Func<T> code)
      {
          try
          {
              var result = code != null ? code.Invoke() : default(T);
              transaction.Commit();
              return result;
          }
          catch
          {
              transaction.Rollback();
              throw;
          }
      }
      

      【讨论】:

      • 我希望 Dispose 已被定义为采用 Exception 类型的参数,该参数将指示 try/finally 上下文(如果有)有哪些异常(如果有)待处理 保护对象。查询线程是否存在未决异常并不是一回事,因为 Dispose 本身可能会在 try/finally 块中被调用,该块嵌套在保护要处置的对象的块中。
      【解决方案5】:

      您可以购买实现“MyWrapper”类的 Dispose 方法。在dispose方法中可以查看是否有异常如下

      public void Dispose()
      {
          bool ExceptionOccurred = Marshal.GetExceptionPointers() != IntPtr.Zero
                                   || Marshal.GetExceptionCode() != 0;
          if(ExceptionOccurred)
          {
              System.Diagnostics.Debug.WriteLine("We had an exception");
          }
      }
      

      【讨论】:

        【解决方案6】:

        如果你想完全停留在 .net 中,我建议的两种方法是编写一个“try-catch-finally”包装器,它可以接受不同部分的委托,或者编写一个“使用风格”包装器,它接受一个要调用的方法,以及一个或多个 IDisposable 对象,这些对象应该在它完成后被释放。

        “使用风格”包装器可以在 try-catch 块中处理处置,如果在处置中抛出任何异常,则将它们包装在 CleanupFailureException 中,该异常将保存处置失败以及发生在主委托,或者使用原始异常向异常的“数据”属性添加一些内容。我倾向于将事物包装在 CleanupFailureException 中,因为在清理过程中发生的异常通常表明比在主线处理中发生的问题要大得多;此外,可以编写 CleanupFailureException 以包含多个嵌套异常(如果有“n”个 IDisposable 对象,则可能有 n+1 个嵌套异常:一个来自主线,一个来自每个 Dispose)。

        用 vb.net 编写的“try-catch-finally”包装器虽然可以从 C# 调用,但可能包含一些 C# 中不可用的功能,包括将其扩展为“try-filter-catch-fault”的能力-finally”块,其中“过滤器”代码将在堆栈从异常中解开之前执行并确定是否应捕获异常,“故障”块将包含仅在发生异常时运行的代码,但会实际上并没有捕获它,并且“错误”和“最终”块都将接收参数,指示在“尝试”执行期间发生了什么异常(如果有),以及“尝试”是否成功完成(注意,顺便说一句,即使主线完成,异常参数也可能为非空;纯 C# 代码无法检测到这种情况,但 vb.net 包装器可以)。

        【讨论】:

          【解决方案7】:

          ,在.Net 框架中没有办法做到这一点,你无法弄清楚finally 子句中抛出的当前异常。

          请参阅此post on my blog,与 Ruby 中的类似模式进行比较,它突出了我认为 IDisposable 模式存在的差距。

          Ayende 有一个技巧可以让你detect an exception happened,但是它不会告诉你这是哪个异常。

          【讨论】:

          • 在“检测异常发生”网站上很好地发现了该黑客无法与中等信任的 asp.net 一起使用。
          【解决方案8】:

          不仅可以查明在处置一次性对象时是否引发了异常,您甚至可以通过一点魔法来掌握 finally 子句中引发的异常。 我的 ApiChange 工具的跟踪库使用此方法来跟踪 using 语句中的异常。可以在here找到更多信息。

          你的, 阿洛伊斯·克劳斯

          【讨论】:

          • 能否区分以下情况:(1) 在 using 语句中捕获并处理(不重新抛出)异常,(2) 在 using 语句中未捕获异常,以及 @987654323 @ 在展开堆栈时被调用,或者 (3) 未在 using 语句中捕获的异常在其外部被首次通过捕获,但是在展开堆栈时发生另一个异常,该异常在 using 语句中被捕获和处理,所以Dispose 之后的执行将遵循“正常”路径。我真的希望using 支持使用参数的IDisposable 导数...
          • ...表示它是在“正常”还是“异常”上下文中使用,因为我认为在Dispose 过程中没有其他任何语义一致的方式来说明.该构造会有所帮助的一个重要案例是锁定/互斥构造。如果异常使受互斥锁保护的资源处于不良状态,则不应允许尝试使用该资源,但也不应永远阻塞(或保持阻塞)。相反,他们应该抛出异常。如果可以通过using 可以区分正常退出与异常的守卫来实现这一点,那就太好了。
          • 我没有安装异常过滤器,只是检查是否存在异常指针结构,该结构指示 dispose 调用中的堆栈展开场景。这是场景 2。场景 1 当然也包括在内,因为您可以决定是否展开堆栈。也可以做 3 次,但这可能会非常昂贵,所以我还没有尝试过。
          • 案例 3 是一个奇怪的案例,通常不应该发生,但至少知道如果发生会发生什么可能是件好事。否则,在由于异常调用而执行的catchfinally 块在嵌套的try 块或其finally 块中调用Dispose 的情况下,异常指针结构将指示什么(所以在Dispose 时,内部块可以预期正常退出,而外部块通过异常退出)?真正需要的是finally 接收挂起的异常参数,using 支持...
          • ...IDisposable 的派生或变体,其中包括一个(最佳方案是存在不继承 IDisposable 的变体,以及继承 IDisposable 和该变体。因此,为了正确操作需要更高级用法的类可以将自己声明为仅实现变体接口,而不是使用 using 与旧式编译器进行编译。
          【解决方案9】:

          James,wrapper 所能做的就是记录它自己的异常。您不能强制wrapper 的使用者记录他们自己的异常。这不是 IDisposable 的用途。 IDisposable 用于对象资源的半确定性释放。编写正确的 IDisposable 代码并非易事。

          事实上,类的使用者甚至不需要调用你的类的 dispose 方法,也不需要使用 using 块,所以这一切都被打破了。

          如果您从包装类的角度来看它,为什么要关心它存在于 using 块中并且存在异常?这会带来什么知识?让第 3 方代码了解异常详细信息和堆栈跟踪是否存在安全风险?如果计算中有被零除,wrapper 能做什么?

          无论 IDisposable 是什么,记录异常的唯一方法是 try-catch,然后重新抛出 catch。

          try
          {
              // code that may cause exceptions.
          }
          catch( Exception ex )
          {
             LogExceptionSomewhere(ex);
             throw;
          }
          finally
          {
              // CLR always tries to execute finally blocks
          }
          

          您提到您正在创建一个外部 API。您必须在 API 的公共边界使用 try-catch 包装每个调用,以便记录异常来自您的代码。

          如果您正在编写公共 API,那么您真的应该阅读 Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (Microsoft .NET Development Series) - 2nd Edition .. 1st Edition


          虽然我不提倡它们,但我看到 IDisposable 用于其他有趣的模式:

          1. 自动回滚事务语义。如果尚未提交,事务类将回滚 Dispose 上的事务。
          2. 用于记录的定时代码块。在对象创建期间记录时间戳,在 Dispose 时计算 TimeSpan 并写入日志事件。

          * 这些模式可以通过另一层间接和匿名委托轻松实现,而无需重载 IDisposable 语义。重要的一点是,如果您或团队成员忘记正确使用它,您的 IDisposable 包装器将毫无用处。

          【讨论】:

          • 自动回滚事务是我对此感兴趣的主要原因。本质上,这样我就可以拥有一个 using 块,而不必在最后显式提交,但仍然允许它在异常时中止。我有两种想法,是否明确表达更好。 MS 使用显式的 System.Transactions,但我觉得开发人员可能会忘记提交。
          • 好吧,@tjmoore,你会注意到我说的是自动回滚,而不是自动提交。所以在这种情况下,如果没有明确提交,包装器 Dispose() 方法将回滚。但是,我不提倡这种模式,因为它是 IDisposable 的混蛋。借助将匿名委托作为参数传递给方法的能力,您可以轻松引入另一层间接层,以更合适的方式实现相同的结果。
          • @tjmoore:如果必须强制提交,则离开 using 块而不调用它应该会触发异常除非因为抛出异常而离开块在这种情况下通过抛出另一个异常来破坏第一个异常是非常粗鲁的。
          【解决方案10】:

          这将捕获直接或在 dispose 方法中抛出的异常:

          try
          {
              using (MyWrapper wrapper = new MyWrapper())
              {
                  throw new MyException("Bad error.");
              }
          }
          catch ( MyException myex ) {
              //deal with your exception
          }
          catch ( Exception ex ) {
              //any other exception thrown by either
              //MyWrapper..ctor() or MyWrapper.Dispose()
          }
          

          但这依赖于他们使用此代码 - 听起来您希望 MyWrapper 改为这样做。

          using 语句只是为了确保 Dispose 总是被调用。真的是这样做的:

          MyWrapper wrapper;
          try
          {
              wrapper = new MyWrapper();
          }
          finally {
              if( wrapper != null )
                  wrapper.Dispose();
          }
          

          听起来你想要的是:

          MyWrapper wrapper;
          try
          {
              wrapper = new MyWrapper();
          }
          finally {
              try{
                  if( wrapper != null )
                      wrapper.Dispose();
              }
              catch {
                  //only errors thrown by disposal
              }
          }
          

          我建议在您的 Dispose 实施中处理这个问题 - 您应该在 Disposal 期间处理任何问题。

          如果您占用了一些资源,您需要 API 的用户以某种方式释放它,请考虑使用 Close() 方法。你的 dispose 也应该调用它(如果还没有调用的话),但是如果你的 API 的用户需要更好的控制,他们也可以自己调用它。

          【讨论】:

            【解决方案11】:

            而不是 using 语句的语法糖,为什么不为此实现自己的逻辑。比如:

            try
            {
              MyWrapper wrapper = new MyWrapper();
            
            }
            catch (Exception e)
            {
              wrapper.CaughtException = true;
            }
            finally
            {
               if (wrapper != null)
               {
                  wrapper.Dispose();
               }
            }
            

            【讨论】:

            • 我认为您的总体建议很好,但是您的具体实现不会捕获 Dispose() 方法中抛出的异常,您需要包装 finally 块的内容(或者只是Dispose() call) 在它自己的 try/catch 块中。
            • @MikeB 包装类将知道它是否有异常。如果需要,它可以在内部实现 try-catch。
            • MyWrapper 应该在 try/catch 块之外进行实例化,或者应该以不同的方式重写 try/catch。如果 MyWrapper 抛出异常,则在 catch 块中可能会出现 NullReferenceException。
            • @Robert Paulson - 对。我误解了原来的问题。
            • @MikeB 你第一次是对的。我想要一种方法来找出嵌套在 using 语句中的代码中是否引发了异常。我还想保留良好的 using 语法,因为这是针对外部 API 的。
            猜你喜欢
            • 1970-01-01
            • 2011-05-13
            • 1970-01-01
            • 2016-03-14
            • 1970-01-01
            • 1970-01-01
            • 2020-08-07
            • 2014-07-19
            • 1970-01-01
            相关资源
            最近更新 更多