嵌套的Try/Catch

18

如果有一个嵌套的Try/Catch,这是否意味着你的代码不够简洁?我在想这个问题,因为在我的catch中,我调用了另一个方法,如果该方法失败,我会得到另一个运行时错误,所以我想用另一个try/catch来包装这些调用。我想知道这样做是否正常?

例如:

    catch (Exception ex)
    {
        transaction.VoidOrder(transactionID);

        LogError(ex.ToString());
        Response.Redirect("Checkout", false);
    }

因此,VoidOrder甚至LogError方法可能会崩溃。现在,当我调用VoidOrder时,由于它调用了一个BL方法并且在该BL方法中重新抛出异常,因此我会在transactionID上获得null引用,在上面的代码中,我可以捕获它。但是,如果在catch块内再次抛出异常,则我需要捕获它。


希望这只是伪代码,但你最好也不要捕获通用异常。 - The Matt
2
只要您先尝试捕获特定的异常(因为您可以在每个try中有多个catch块),那么捕获一般的异常作为通配符是可以的。仅以上内容还不够具体... - KP.
就算不值钱,我也会先记录下错误。如果Transaction.voidorder出现异常,那么你就没有任何记录了。 - Christian Payne
@KP - 你打算如何处理那个通用异常?显然它没有被预料到,因为你没有特别捕获它,但如果除了终止应用程序之外还有其他操作,那么你会遇到麻烦,因为继续运行应用程序将使其处于不稳定状态。 - The Matt
@Matt 我同意你不应该让应用程序继续运行。我的观点是,你仍然应该捕获、处理/记录异常,而不是不做任何操作,让应用程序因为某些原因崩溃。由于这种异常可能是通用的,所以你可能需要记录并重定向到一个通用的错误页面,该页面会记录用户退出等操作,以保护不稳定的会话。 - KP.
5个回答

15

我们处理这个问题的方法如下:

UI / codebehind 层级中通往其他层级的所有调用都使用 try-catch,其中我们始终捕获自定义异常。底层执行的所有操作都有自己的 try-catch,会记录、包装并抛出自定义异常。然后 UI 可以依赖此功能,并寻找带有友好错误消息的已处理异常。

Codebehind:

protected void btnSubmit_Click(object sender, EventArgs e)
{
    //do something when a button is clicked...
    try
    {
        MyBL.TakeAction()
    }
    catch(MyApplicationCustomException ex)
    {
        //display something to the user, etc.
        ltlErrorPane.Text = ex.Message;

        //or redirect if desired
        if(ex.ErrorType == MyCustomErrorsType.Transactional)
        {
            Response.Redirect("~/Errors/Transaction.aspx");
        }
    }
}

业务层:

在业务层中,任何可能失败的操作都会使用try-catch块进行处理,该块会在将问题抛到用户界面之前记录并封装问题。

public class MyBL
{
    public static void TakeAction()
    {
        try
        {
            //do something
        }
        catch(SpecificDotNetException ex)
        {
            //log, wrap and throw
            MyExceptionManagement.LogException(ex)
            throw new MyApplicationCustomException(ex, "Some friendly error message", MyCustomErrorsType.Transactional);
        }
        finally
        {
            //clean up...
        }
    }
}

异常处理程序:

实际的异常处理程序有多种记录方式,包括事件日志、文件日志和最后一项是电子邮件(如果其他方法都无法使用)。我们选择在记录器无法执行任何预期操作时简单地返回 false。尽管这是个人选择,但我们认为三种方法依次失败的可能性非常小(事件日志失败,尝试文件日志,失败,尝试电子邮件,失败)。在这种情况下,我们选择允许应用程序继续运行。另一个选项是允许应用程序完全失败。

public static class MyExceptionManagement
{
    public static bool LogException(Exception ex)
    {
        try
        {
            //try logging to a log source by priority, 
            //if it fails with all sources, return false as a last resort
            //we choose not to let logging issues interfere with user experience

            //if logging worked
            return true;
        }
        catch(Exception ex)
        {
            //in most cases, using try-catch as a true-false is bad practice
            //but when logging an exception causes an exception itself, we do
            //use this as a well-considered choice.
            return false;
        }
    }
}

最后,作为一种应急措施,我们还在Global.asax中实现了Application_Error全局事件处理程序。当我们没有正确地try-catch某些内容时,这是最后的手段。一般情况下,我们会记录并重定向到友好的错误页面。如果以上自定义错误处理成功完成,很少有错误会到达全局处理程序。

希望这可能对您有所帮助。这是一个可能的解决方案。在一些较大的应用程序上,它已经成功运行了几年。


OP正在询问如何处理捕获块中的命令引发异常的情况 - 例如,//log尝试写入一个不再存在的文件系统上的日志文件。 - Kris C
是的,但如果您不希望BL抛出异常,并且希望在任何主要或次要错误时将用户重定向回结帐,则该怎么办?如果在BL中抛出显式错误,则除非从BL重新抛出,否则无法重定向它们。因此,您可以在BL中使用try/catch,但需要抛出并让代码后台优雅地捕获和处理。 - PositiveGuy
2
你的用户界面(UI)应该进行重定向,而不是业务逻辑(BL)。如果是这种情况,你必须以某种方式告诉你的UI问题所在,并让它进行重定向。自定义异常是一个完美的例子。捕获它,然后重定向。如果需要,你也可以有不同的自定义异常(例如,一个用于事务问题,一个用于x问题,一个用于y问题)。另一种选择是向UI返回一个值,例如状态枚举或布尔值。无论哪种方式,你都必须通知UI。 - KP.
如果您真的认为BL应该通过HttpContext.Current.Response.Redirect进行重定向,那么可以这样做。您可以像我的示例代码一样在BL中记录,然后重定向。个人而言,我宁愿让UI做出UI决策,而BL只是告诉UI问题所在。在我看来,保持事物分割更好。BL应处理业务决策(事务、验证、保存、获取等),而不知道UI的情况。理想情况下,您应该能够在不重构BL的情况下将UI切换到Windows窗体。当BL调用HttpContext.Current.Response.Redirect时,您无法执行此操作。 - KP.

5

嵌套的try/catch块在需要记录日志、发送消息或以其他非平凡方式响应异常的顶级应用程序中是不可避免的。

有一些方法可以减少嵌套块的数量(例如,使用ASP.NET的HttpApplication.Error处理程序(又称Application_Error)来 consololidate 错误处理),但您应该捕获由处理代码产生的任何异常,并在万不得已时实施备份计划。


1

解决嵌套问题的另一个方法是在内部函数(LogError等)中封装try/catch逻辑,而不是依赖调用者来捕获它们。对于LogError来说,这是有意义的,因为您可能希望以相同的方式处理破损的错误记录器,无论谁试图记录错误。


1

也许有人会问为什么要使用嵌套的try/catch语句。有时候不得不使用它们,但我认为通常情况下并不美观。
你应该考虑到关注点分离是必须的。

一个getControl方法不应该插入脚本,一个save方法也不应该做更多的事情除了保存。

检查这些方法确切应该做什么,然后你就会得到答案。如果出现错误将订单设置为空看起来并不奇怪。听起来很合理。


0
你有没有考虑过使用TransactionScope对象来代替手动回滚事务?我知道有时这是不可能的,但大多数情况下,TransactionScope对我来说都非常有效。这样,我就不必手动管理回滚项——手动回滚可能会成为一个问题,因为数据处于“不稳定”状态,这可能会破坏你的整个逻辑。

1
我不需要回滚。我正在发送SOAP调用。如果有任何失败,将用户重定向回结账页面。 - PositiveGuy
抱歉,我误解了您的VoidOrder,认为它是一种回滚,因为您必须处理内存中的某些事务数据。回答您最初的问题,似乎您需要在BL中加入更多的保护性代码。正如您所指出的,今天您会遇到空引用异常,这可能需要您进行适当的处理。您将会遇到这样一种情况,即您从应用程序中获得的唯一错误是真正意外的错误,无法恢复,那么我会使用上面建议的重新抛出模式。 - Wagner Silveira

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