如何确定.NET异常是否被处理?

16

我们正在研究C#中的一种编码模式,希望在一个特殊类上使用 "using" 语句,该类的 Dispose() 方法根据 "using" 语句是否正常退出或异常退出执行不同的操作。

据我所知,CLR跟踪当前处理的异常,直到被 "catch" 处理程序消耗掉。但并不清楚这些信息是否以任何方式暴露给代码来访问。您是否知道它是否可以访问,如果可以,如何访问?

例如:

using (var x = new MyObject())
{
    x.DoSomething();
    x.DoMoreThings();
}

class MyObject : IDisposable
{
    public void Dispose()
    {
        if (ExceptionIsBeingHandled)
            Rollback();
        else
            Commit();
    }
}

这看起来几乎像是System.Transactions.TransactionScope,不同之处在于成功/失败并不是通过调用x.Complete()来确定,而是基于using体是否正常退出。


2
我认为你需要问一下自己为什么要这样做。Dispose()模式并不是通过引发异常来实现控制逻辑的。 - Mitch Wheat
1
质疑整个想法总是一个公正的观点。我理解这不是“使用”应该被使用的方式。我知道这可能会导致糟糕的代码。但我仍然对答案感兴趣 :) - Roman Starkov
"using"语句被转换为'try/finally'块并且对象被处理。 - SoftwareGeek
5个回答

13

这篇文章描述了一种“hack”方法,用于检测代码是否在异常处理模式下执行。它使用Marshal.GetExceptionPointers来查看是否存在“活动”的异常。

但请注意:

备注

GetExceptionPointers仅用于结构化异常处理(SEH)的编译器支持。 注:此方法使用SecurityAction.LinkDemand以防止从不受信任的代码中调用;只有直接调用者需要具有SecurityPermissionAttribute.UnmanagedCode许可。如果您的代码可能被部分信任的代码调用,请勿将用户输入传递给Marshal类方法而不进行验证。有关使用LinkDemand成员的重要限制,请参见Demand vs. LinkDemand。


5
这可能是你问题的唯一真正答案,但追求它似乎是一个非常糟糕的想法。 - Paul Turner
谢谢,非常准确。@编程英雄:有可能。不能否认看起来是这样的。我们将在一个小测试项目中尝试一下这种方法,看看是否存在重大问题。 - Roman Starkov
3
如果你选择这条路,先看一下以下博客:http://geekswithblogs.net/akraus1/archive/2008/04/08/121121.aspx - Joe
2
使用这种方法,你在小型测试项目中不会看到主要问题。当你的代码部署到有着数千个独特和不同配置怪癖的机器上时,你才会看到问题。 - Anton Tykhyy
对于任何考虑使用这个hack的人,我想提醒一下:由于我们最终没有使用它,它仍未经过“野外”测试;请参见我的下面的帖子,了解我们实际采用的替代方案的示例。 - Roman Starkov

8

这不是对问题的回答,只是一个注释,我从未在实际代码中使用“accepted” hack,因此它仍然在“野外”中大部分未经测试。相反,我们采用了类似于以下内容的方法:

DoThings(x =>
{
    x.DoSomething();
    x.DoMoreThings();
});

在哪里

public void DoThings(Action<MyObject> action)
{
    bool success = false;
    try
    {
        action(new MyObject());
        Commit();
        success = true;
    }
    finally
    {
        if (!success)
            Rollback();
    }
}

重点是它与问题中的“using”示例一样紧凑,不使用任何黑科技。
缺点之一是性能有所下降(在我们的情况下完全可以忽略不计),以及当我实际上想要直接跨越x.DoSomething()而不是进入DoThings时,按F10会进入DoThings。这两个问题都非常小。

3
由于与匿名方法有关的IntelliSense错误,这在VS2010中变得不那么吸引人了... https://connect.microsoft.com/VisualStudio/feedback/details/557224/intellisense-completely-fails-for-collection-inside-anonymous-method?wa=wsignin1.0 - Roman Starkov

4

这些信息对您不可用。

我建议使用类似于DbTransaction类的模式:也就是说,您的IDisposable类应该实现一个类似于DbTransaction.Commit()的方法。然后,您的Dispose方法可以根据是否调用了Commit来执行不同的逻辑(在DbTransaction的情况下,如果没有显式提交事务,则事务将回滚)。

您的类的用户将使用以下模式,类似于典型的DbTransaction:

using(MyDisposableClass instance = ...)
{
    ... do whatever ...

    instance.Commit();
} // Dispose logic depends on whether or not Commit was called.

编辑我看到你修改了问题并且表明你知道这个模式(你的示例使用了TransactionScope)。然而,我认为这是唯一现实可行的解决方案。


2
一个using语句只是try finally块的语法糖。您可以通过完整编写try finally块并添加catch语句来处理特殊情况来实现您想要的效果:
try
{
    IDisposable x = new MyThing();
}
catch (Exception exception) // Use a more specific exception if possible.
{
    x.ErrorOccurred = true; // You could even pass a reference to the exception if you wish.
    throw;
}
finally
{
    x.Dispose();
}

如果您愿意,您可以在 MyThing 内部执行以下操作:

class MyThing : IDisposable
{
    public bool ErrorOccurred() { get; set; }

    public void Dispose()
    {
        if (ErrorOccurred) {
            RollBack();
        } else {
            Commit();
        }
    }
}

注意:我也不得不想知道为什么你想这样做。这有些代码气味。Dispose方法旨在清理非托管资源,而不是处理异常。您最好在catch块中编写异常处理代码,而不是在dispose中,并且如果您需要共享代码,请编写一些有用的帮助函数,可以从两个位置调用。

以下是更好地实现您想要的方式:

using (IDisposable x = new MyThing())
{
    x.Foo();
    x.Bar();
    x.CommitChanges();
}

class MyThing : IDisposable
{
    public bool IsCommitted { get; private set; }

    public void CommitChanges()
    {
        // Do stuff needed to commit.
        IsCommitted = true;
    }

    public void Dispose()
    {
        if (!IsCommitted)
            RollBack();
    }
}

啊,我应该提到这一点。问题在于进入“catch”和“finally”的代码是相同的,但也不是微不足道的。重点是将此代码移动到其他地方。 - Roman Starkov
这没问题。Dispose 代码在两种情况下都会被调用,但是在 catch 代码中,您可以在调用 dispose 之前执行额外的代码。例如,您可以在 x 中设置一些布尔值,在调用 Dispose 时可以进行检查。 - Mark Byers
感谢您的努力,但如果我们仍然需要放置“常规”的try/catch子句,那么这就没有意义了 - 正如您所正确观察的那样,出现在if(ErrorOccurred)/else中的内容应该直接放在catch/ finally中。这个想法是通过使用“using”来节省重复的try/catch/finally,因为它将减少各处的样板代码。 - Roman Starkov
我已更新我的评论,请看它的结尾(现在非常长)。不再使用try catch块,只有额外的commit语句。 - Mark Byers

2
这个想法并不算太糟糕,只是在C#/.NET中不太理想。在C++中有一个函数可以使代码检测到它是否由于异常而被调用。这对于RAII析构函数非常重要; 对于析构函数来说,根据控制流是正常还是异常选择提交或中止是微不足道的。我认为这是一个相当自然的方法,但缺乏内置支持(以及解决方法的道德可疑性;对我来说感觉相当依赖于实现)可能意味着应采取更常规的方法。


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