IDisposable.Dispose方法中有没有办法弄清楚是否抛出异常?

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

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

有帮助吗?

解决方案

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

请参阅此在我的博客上发帖,以进行比较在Ruby中使用类似的模式,它突出了我认为与IDisposable模式存在的差距。

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

其他提示

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

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

如果在using语句中抛出异常,则Dispose之前不会调用AppDomain.CurrentDomain.FirstChanceException

如果你想知道抛出了什么确切的异常,那么订阅ThreadLocal<Exception>事件并在TransactionScope变量中存储最后抛出的异常。

此类模式在<=>类中实现。

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

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

事实上,该类的消费者甚至不需要调用您的类dispose方法,也不需要使用using块,所以它们都会崩溃。

如果从包装类的角度来看它,为什么它应该关心它是否存在于一个使用块中并且有一个例外?这会带来什么知识?让第三方代码知道异常细节和堆栈跟踪是否存在安全风险?如果计算中存在除零,<=>可以做什么?

记录异常的唯一方法是,无论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。您必须使用try-catch在API的公共边界处包装每个调用,以便记录该异常来自您的代码。

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


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

  1. 自动回滚事务语义。如果尚未提交,事务类将在Dispose上回滚事务。
  2. 用于记录的定时代码块。在对象创建期间记录了一个时间戳,并且在Dispose上计算了TimeSpan并写入了一个日志事件。
  3. *这些模式可以通过另一层间接和匿名委托轻松实现,而无需重载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();
   }
}

不仅可以找出在处理一次性对象时是否抛出了异常,你甚至可以用一点魔法来触及在finally子句中抛出的异常。 ApiChange工具的My Tracing库使用此方法跟踪using语句中的异常。更多信息如何工作可以在这里找到。

此致,    Alois Kraus

现在,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的实现中处理这个问题 - 无论如何你应该在Disposal中处理任何问题。

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

如果你想纯粹留在.net中,我建议的两种方法是写一个<!> quot; try-catch-finally <!> quot;包装器,它将接受不同部分的代理,或者写一个<!> quot; using-style <!> quot; wrapper,接受一个要调用的方法,以及一个或多个IDisposable对象,它们应该在完成后处理。

A <!> quot; using-style <!> quot; wrapper可以处理try-catch块中的处理,并且如果处理中抛出任何异常,则将它们包装在CleanupFailureException中,这将保留处理失败以及主委托中发生的任何异常,或者添加一些内容例外的<!>“;数据<!>”;具有原始例外的财产。我赞成在CleanupFailureException中包装东西,因为清理期间发生的异常通常表明比主线处理中出现的问题大得多;此外,可以编写一个CleanupFailureException以包含多个嵌套异常(如果有'n'个IDisposable对象,则可能有n + 1个嵌套异常:一个来自主线,另一个来自每个Dispose)。

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

就我而言,我想在微服务崩溃时进行记录。我已经有一个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);
       }
  }

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top