在 IDisposable.Dispose 中截获异常

本文关键字:异常 IDisposable Dispose | 更新日期: 2023-09-27 17:47:21

IDisposable.Dispose方法中,有没有办法确定是否抛出了异常?

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

如果在 using 语句中抛出异常,我想在释放IDisposable对象时了解它。

在 IDisposable.Dispose 中截获异常

您可以使用

方法 Complete 扩展IDisposable并使用如下模式:

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

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

如果您想知道抛出的确切异常,请订阅AppDomain.CurrentDomain.FirstChanceException事件并将上次引发的异常存储在变量ThreadLocal<Exception>

这种模式在类TransactionScope实现。

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

请参阅我博客上的这篇文章,与 Ruby 中的类似模式进行比较,它突出了我认为 IDisposable 模式存在的差距。

Ayende有一个技巧,可以让您检测到发生的异常,但是,它不会告诉您它是哪个异常。

无法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() 并执行一些日志记录的方法。这样,您并不总是需要尝试/捕获块。

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;
    }
}

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

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

如果你从包装类的角度来看它,为什么它应该关心它存在于一个use块中并且有一个例外?这带来了什么知识?让第三方代码了解异常详细信息和堆栈跟踪是否存在安全风险?如果计算中存在除以零,wrapper该怎么办?

记录异常的唯一方法(无论 ID是否可用)是 try-catch,然后重新抛出 catch。

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

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

如果你正在编写一个公共API,那么你真的应该阅读框架设计指南:可重用.NET库的约定,习语和模式(Microsoft.NET开发系列)-第2版..第1版。


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

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

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

你可以这样做,实现"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");
    }
}

与其使用 using 语句的语法糖,为什么不为此实现自己的逻辑。 像这样:

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

就我而言,我想这样做是为了记录微服务崩溃的时间。我已经制定了在实例关闭之前正确清理的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);
       }
  }

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

现在,在 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;
            });

这将捕获直接或在 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 时处理这个问题 - 无论如何,您应该在处置过程中处理任何问题。

如果你正在占用一些资源,你需要你的API用户以某种方式释放它,请考虑使用Close()方法。你的处置也应该调用它(如果还没有),但如果你的 API 用户需要更精细的控制,也可以自己调用它。

如果你想纯粹停留在.net中,我建议的两种方法是编写一个"try-catch-final"包装器,它将接受不同部分的委托,或者编写一个"using-style"包装器,它接受要调用的方法,以及一个或多个应在完成后释放的IDisposable对象。

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

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