在IDisposable.Dispose中拦截异常

43

IDisposable.Dispose 方法中是否有一种方法可以确定是否抛出异常?

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

如果在using语句中抛出异常,我希望在IDisposable对象被处理时得到通知。


你的实现IDisposable接口的包装器为什么需要知道它被释放的原因? - Robert Paulson
5
我想写一个记录日志的消息,知道代码在语句中是否成功运行或者由于异常中途跳出。请帮我翻译这句话。 - James Newton-King
看看这个来自ayende的内容:http://ayende.com/Blog/archive/2007/06/20/Did-you-know-Find-out-if-an-exception-was-thrown.aspx - Simon Laroche
10个回答

26
你可以使用 Complete 方法扩展 IDisposable ,并使用类似以下的模式:
using (MyWrapper wrapper = new MyWrapper())
{
    throw new Exception("Bad error.");
    wrapper.Complete();
}

如果在using语句内部抛出异常,则在Dispose之前不会调用Complete函数。
如果想知道具体抛出了什么异常,可以订阅AppDomain.CurrentDomain.FirstChanceException事件,并将最后一个抛出的异常存储在ThreadLocal<Exception>变量中。
这种模式实现在TransactionScope类中。

是的,这是非常好的观点:AppDomain.CurrentDomain.FirstChanceException,再加上一些ThreadStatic魔法,可能还有类似于using(ExceptionTracker.Begin()) { if(ExceptionTracker.ThrownExceptions.Any()) { .. } }的东西。这个魔法的关键在于,在该上下文中的任何代码都应该可以通过ThrownExceptions访问抛出的异常,而且它们不需要传递任何参数。重点是,如果打开了异常跟踪上下文,那么这些异常将对内部代码可用,一旦关闭了该上下文,跟踪就可以被取消挂钩,并清除异常列表。 - Demetris Leptos

21
不行,在 .Net Framework 中无法在 finally 子句中找出当前正在抛出的异常。参见我博客上这篇文章,与 Ruby 中类似模式进行比较,它强调了我认为 IDisposable 模式存在的差距。Ayende 有一个技巧可以让你检测到异常发生,但它无法告诉你是哪个异常。

在“检测异常发生”的站点上,你很好地发现了这个黑客无法在中等信任的asp.net中工作。 - TamusJRoyce
1
不幸的是,Marshal.GetExceptionCode() 的未来并不光明:在未来的版本中,GetExceptionCode() 可能无法使用。Marshal.GetExceptionCode Method - t3chb0t

6

Dispose()方法中无法捕获异常。

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

如果您不需要一个类,只想捕获异常,可以创建一个接受lambda表达式的函数,并在try/catch块中执行该表达式,类似于以下内容:

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

2
我希望Dispose被定义为接受一个类型为Exception的参数,该参数将指示在try/finally上下文(如果有)保护对象时是否有任何异常待处理。询问线程是否有待处理的异常并不完全相同,因为Dispose本身可能会在嵌套在保护要处理的对象的try/finally块内的try/finally块中调用。 - supercat

3
詹姆斯,wrapper只能记录自己的异常。你无法强制使用wrapper的消费者记录他们自己的异常。这不是IDisposable的用途。IDisposable用于半确定性释放对象资源。编写正确的IDisposable代码并不容易。
实际上,类的使用者甚至不需要调用您的类的dispose方法,也不需要使用using块,所以一切都变得混乱了。
如果从包装器类的角度来看,为什么它要关心它是否存在于using块中并且有异常?那会带来什么知识?让第三方代码知道异常详细信息和堆栈跟踪是否存在安全风险?如果计算中出现除零错误,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包装器将无用。


1
自动回滚事务是我感兴趣的主要原因。基本上,这样我就可以使用块而不必在结尾显式提交,但仍然允许在异常情况下中止。我对是否明确更好有两种看法。微软在System.Transactions中采用了明确的方式,但我觉得开发人员可能会忘记提交。 - tjmoore
@tjmoore,你会注意到我说的是自动回滚(Auto-Rollback),而不是自动提交(Auto-Commit)。因此,在这种情况下,如果没有明确提交,包装器的Dispose()方法将回滚。然而,我不赞成这种模式,因为它是IDisposable的变形。通过在方法中传递匿名委托作为参数,您可以轻松地引入另一层间接性,以更合适的方式实现相同的结果。 - Robert Paulson
如果一个提交应该是强制性的,那么离开 using 块而不调用它应该会触发一个异常,除非你离开块是因为抛出了异常,在这种情况下通过抛出另一个异常来销毁第一个异常将是非常粗鲁的。 - supercat

2
您可以通过实现"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");
    }
}

1

在我的情况下,我想要记录微服务崩溃的情况。在实例关闭之前,我已经使用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);
       }
  }

根据你需要完成的任务,这可能有些限制,但对于我所需的内容来说很有效。

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

1

不妨尝试自己实现逻辑,而非使用using语句的语法糖。可以像这样:

try
{
  MyWrapper wrapper = new MyWrapper();

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

我认为你的整体建议很好,但是你的具体实现无法捕获在Dispose()方法中抛出的异常,你需要将finally块的内容(或者只是Dispose()调用)包装在它自己的try/catch块中。 - Michael Burr
@MikeB 包装类会知道它是否有异常。如果需要,它可以在内部实现 try-catch。 - Robert Paulson
MyWrapper 应该在 try/catch 块之外实例化,或者需要以不同的方式重写 try/catch。如果 MyWrapper 抛出异常,则在 catch 块中可能会出现 NullReferenceException。 - Richard Nienaber
@Robert Paulson - 对的。我误解了原始问题。 - Michael Burr
@MikeB 你第一次说的对。我想找到一种方法来了解代码中嵌套在using语句内部的异常是否被抛出。同时,我也想保留漂亮的using语法,因为这是针对外部API的。 - James Newton-King

0

这将捕获直接抛出或在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用户也可以自行调用它。

0

如果您想保持纯粹的.NET环境,我会建议两种方法:一是编写一个"try-catch-finally"封装器,该封装器将接受不同部分的委托,或者编写一个“using-style”封装器,该封装器需要一个要调用的方法以及一个或多个IDisposable对象,在完成后应进行处理。

一个“using-style”封装器可以在try-catch块中处理处理,并且如果在处理过程中抛出任何异常,则将它们包装在CleanupFailureException中,其中包含了处理失败以及发生在主委托中的任何异常,或者将其添加到异常的“Data”属性中,该异常属于原始异常。 我更倾向于将事物包装在CleanupFailureException中,因为在清理过程中发生的异常通常会比在主线处理中发生的异常更严重;此外,CleanupFailureException可以编写为包括多个嵌套异常(如果有“n”个IDisposable对象,则可能有n + 1个嵌套异常:一个来自主线和每个Dispose)。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接