判断是否由于抛出异常而在finally块中执行

38

有没有办法确定代码当前是否正在执行 finally 处理程序以响应抛出的异常?我非常喜欢使用 IDisposable 模式来实现进入/退出作用域功能,但是这种模式的一个问题是,如果在 using 的主体中发生异常,您可能不希望发生范围结束行为。我想要的东西类似于:

public static class MyClass
{
    public static void MyMethod()
    {
        using (var scope = MyScopedBehavior.Begin())
        {
            //Do stuff with scope here
        }
    }
}

public sealed class MyScopedBehavior : IDisposable
{
    private MyScopedBehavior()
    {
        //Start of scope behavior
    }

    public void Dispose()
    {
        //I only want to execute the following if we're not unwinding
        //through finally due to an exception:
        //...End of scope behavior    
    }

    public static MyScopedBehavior Begin()
    {
        return new MyScopedBehavior();
    }
}

除了我可以通过传递委托函数来实现这个目标(将调用包装到特定行为中),但我想知道是否可以使用IDisposable模式来实现。


实际上,似乎已经有人之前提出并回答了这里。 它可以通过一种非常巧妙的方式进行检测。我不会使用那种技术,但了解它是可能的很有趣。

8个回答

18

我见过实现这一点的方法需要一个额外的方法:

public static void MyMethod()
{
    using (var scope = MyScopedBehavior.Begin())
    {
        //Do stuff with scope here
        scope.Complete(); // Tells the scope that it's good
    }
}

通过这样做,您的作用域对象可以跟踪它是因为错误还是成功操作而处于释放状态。例如,TransactionScope 就采用了这种方法 (请参阅 TransactionScope.Complete)。


是的,我认为我喜欢你的(乐观)比我的(悲观)更好。 - Brian Genisio
@Brian:两种方法都可以,但这种方法需要使用你的类的用户编写更少的代码 :) - Reed Copsey
这很干净,如果你没有达到“完成”状态,就会发生不同的事情,这更加明确。我更喜欢这种风格而不是我所要求的“技巧”,但我肯定还是很好奇它是否在技术上可行。 - Dan Bryant
@Dan:据我所知(即在BCL中使用),这已经成为处理.NET中分解类型的标准方式。我不知道有什么更好的方法来处理这个问题,因为Dispose()没有提供关于当前异常状态的任何信息。 - Reed Copsey
嵌套作用域无用,实际上也不能检测出异常是否触发了 finally 块。例如,如果这种作用域能像 using(new Scope) using (new Scope) using (new Scope) { call "Complete" on all three scopes } 一样堆叠起来,那么所有作用域都会认为没有发生错误,但实际情况并非如此,因为在最外层作用域的 Dispose 调用时,前两个 Dispose 调用可能会发生错误。所以除了成为 Dispose 永远不会抛出异常的非常有力的论点之外,仍然没有办法“检测”为什么进入了 finally 块。 - Triynko

15

另外一点,IL 允许你指定 SEH fault 块,这类似于 finally 但是只有在抛出异常时才会进入 - 你可以在这里看到一个例子,大约在页面的三分之二处(链接)。不幸的是,C# 没有公开这个功能。


1
+1,这非常有趣 - 你知道有哪些语言可以为CLR公开这个吗?将其用于构建因式类型(甚至是潜在的基类),并将其公开给C#将会很有趣。 - Reed Copsey
2
据我所知没有。VB.NET暴露了“filter”(也在帖子中提到),但C#没有。就像这样的时候,我希望我能扩展C#编译器以添加这个小小的额外语法 :/ - thecoop
1
不幸的是,即使C#暴露了这种行为,它也无法解决OP的问题 :( - Reed Copsey
Boo 支持故障处理程序。 - Nick Cox
1
现在,C#通过when暴露异常过滤器。 - Drew Noakes

7

我正在寻找一些与单元测试类似的东西 - 我有一个帮助类,用于在测试运行后清理对象,并且我想保持漂亮,干净的“using”语法。我还想有不清理失败测试的选项。我想到的方法是调用Marshal.GetExceptionCode()。我不知道这是否适用于所有情况,但对于测试代码来说似乎效果很好。


我的主要用例也是用于单元测试;我想通过Asserts验证某些作用域退出条件,但如果已经抛出了AssertionException(以免抑制它),我想跳过验证。 - Dan Bryant
很不幸的是,它不能在finally块中运行,只能在catch块中运行。 - undefined

5
我能想到的最好翻译是:

我所能提供的最佳翻译可能是:

using (var scope = MyScopedBehavior.Begin())
{
  try
  {
    //Do stuff with scope here
  }
  catch(Exception)
  {
    scope.Cancel();
    throw;
  }
}

当然,scope.Cancel() 确保在 Dispose() 中不会发生任何事情。

我个人更喜欢TransactionScope采用的“Complete”方法,而不是Cancel()方法。它不需要你编写try/catch就可以工作。(详情请参见我的答案...) - Reed Copsey
@Reed Copsey:哈!是的,我认为我们写了答案,然后同时回应了彼此。我同意。你的更好。我不会删掉我的答案,只是为了展示另一种方法。 - Brian Genisio
1
scope.Cancel() 的一个优点是它可以接受一个异常参数,如果 dispose/cleanup 失败,则将其作为内部异常提供。例如,如果事务清理失败,可能会合理地抛出一个比引发事件链的异常“更严重”的异常,但完全失去与原始异常相关的堆栈跟踪和其他信息将是很烦人的。 - supercat
仍然没有达到所需的要求。如果您有多个使用语句堆叠在一起,例如 using (new Scope()) using (new Scope()) using (new Scope()) { mark all three complete },它们都会认为没有发生错误,但是如果从任何一个内部作用域的 finally/dispose 调用中抛出错误,情况就不是这样了。"using" 块本质上是一个残缺的 try/catch/finally 块,不支持 "catch" 块,并且 finally 块被强制成一个简单的 Dispose 调用,哈哈。太糟糕了。 - Triynko

4
以下模式避免了API误用的问题,即范围完成方法未被调用,完全省略或由于逻辑条件而未被调用。我认为这更接近您的问题,并且对于API用户来说代码甚至更少。
编辑
在Dan的评论之后,甚至更加简单直接。
public class Bling
{
    public static void DoBling()
    {
        MyScopedBehavior.Begin(() =>
        {
            //Do something.
        }) ;
    }   
}

public static class MyScopedBehavior
{
    public static void Begin(Action action)
    {
        try
        {
            action();

            //Do additonal scoped stuff as there is no exception.
        }
        catch (Exception ex)
        {
            //Clean up...
            throw;
        }
    }
}   

1
如果选择使用委托来限定行为,我将完全放弃IDisposable并创建一个静态方法MyScope.With(Action<MyScope> action),然后像这样调用它MyScope.With(myScope => { ...delegate contents...}) - Dan Bryant
@Dan 鉴于 API 滥用问题,这可能是更好的解决方案。 - Tim Lloyd
1
如果将范围包装器写成VB.net,它可以接受一个MethodInvoker作为主方法以及一个Action(Of Exception)作为finally子句,后者将获得发生的异常(如果有)用于主操作。与C#不同,VB.net允许您确定发生了什么异常,而无需捕获和重新抛出(这意味着例如调试器陷阱将显示未最终处理的异常发生的位置,而不是它被重新抛出的最低级别)。 - supercat

1

我认为最好的方法是手动编写try/catch/finally子句。从第一本“Effective c#”书中学习一个条目。一个优秀的C#黑客应该清楚地知道using扩展到什么。自.Net 1.1以来,它已经有所改变 - 现在可以有几个using嵌套。因此,使用反射器并研究未精简的代码。

然后,在编写自己的代码时 - 要么使用using,要么编写自己的内容。这并不是非常困难,而且是一个很好的东西。

你可以用其他技巧来装饰它,但感觉太重了,甚至不高效。让我包含一个代码示例。

懒人方式

using (SqlConnection cn = new SqlConnection(connectionString))
using (SqlCommand cm = new SqlCommand(commandString, cn))
{
    cn.Open();
    cm.ExecuteNonQuery();
}

手动方法

bool sawMyEx = false;
SqlConnection cn =  null;
SqlCommand cm = null;

try
{
    cn = new SqlConnection(connectionString);
    cm = new SqlCommand(commandString, cn);
    cn.Open();
    cm.ExecuteNonQuery();
}
catch (MyException myEx)
{
    sawMyEx = true; // I better not tell my wife.
    // Do some stuff here maybe?
}
finally
{
    if (sawMyEx)
    {
        // Piss my pants.
    }

    if (null != cm);
    {
        cm.Dispose();
    }
    if (null != cn)
    {
        cn.Dispose();
    }
}

我真正想要的是使用“using”语法来表达代码意图的清晰度。对于我所描述的情况,如果手动编写代码,实际上根本不需要try/finally,但“using”结构强制执行finally。我主要想知道这个结构可以弯曲到什么程度。 - Dan Bryant
祝你好运,最后一定要保证代码可读性。一旦完成,请发布完整的可工作代码示例。 - Hamish Grubijan
你没有理解丹的观点,但是提供了一个非常搞笑的例子。无论如何,这让我微笑了。 :) - Tim Lloyd
这是我采用的解决方案,因为在我的特定情况下,它是唯一需要的,并且运行良好。因此这是第一个加1。 - ouflak

1

如果有一种IDisposable的变体,它的Dispose方法接受一个参数来指示在运行时是否有任何异常挂起,那将非常有帮助。在其他情况下,如果Dispose无法执行预期的清理操作,它将能够抛出一个包含有关先前异常信息的异常。它还允许Dispose方法抛出异常,如果代码“忘记”在using块中执行应该执行的某些操作,但不会覆盖可能导致使用块过早退出的任何其他异常。不幸的是,目前还没有这样的功能。

有许多文章建议使用API函数来查找是否存在未决异常的方法。这种方法的一个主要问题是,代码可能正在运行try块的finally块中,而该try块已成功完成,但可能嵌套在try块的finally块中,该块提前退出。即使Dispose方法可以确定存在这样的情况,它也无法知道它属于哪个try块。可以制定适用于任一情况的示例。
目前最好的方法可能是具有显式的“成功”方法,并假设如果未调用该方法,则失败,并且即使没有抛出异常,忘记调用“成功”方法的后果也应该是明显的。作为简单实用程序方法可能有用的一件事是,类似于以下内容:
T Success<T>(T returnValue)
{
  Success();
  return T;
}

因此允许像这样的代码:

return scopeGuard.Success(thingThatMightThrow());

rather than

var result = thingThatMightThrow();
scopeGuard.Success();
return result;

0
为什么不在try { }块的最后直接释放资源,而不使用finally?这似乎是您要寻找的行为。
从实际应用的角度来看,这种方式也更加现实。您确定每个使用该类的人都永远不想在出现异常时释放资源吗?或者这种行为应该由类的使用者处理?

这里的Dispose并不是真正意义上释放资源的Dispose,而是通过'using'结构在特定代码块周围添加开始/结束代码的一种语法技巧。可以参考Reed Copsey提到的TransactionScope来了解该模式的示例。 - Dan Bryant

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