.NET的通用异常处理策略

13

我习惯在每个方法中使用try/catch块。这样做的原因是为了在违规点捕获每个异常并记录它。从我的阅读和与他人的交谈中,我理解到这不是一个受欢迎的观点。一个人应该只捕获自己准备处理的异常。然而,如果我不在违规点捕获异常,那么就有可能从未记录该违规点并了解它。注意:当我捕获但不处理异常时,我仍然抛出异常。这使我能够让异常传播到将其处理的地方,同时让我在违规点记录它。

那么...如何避免在每个方法中使用try/catch块,但仍然可以在发生错误的位置记录错误?


3
什么语言?不同的语言有不同的处理从捕获点到抛出点的回溯追踪方式。 - S.Lott
什么驱使您在违规点记录日志?如果您记录了异常中包含的调用堆栈,您仍然可以在日志中看到发生了什么以及您的代码走了哪条路线以最终到达该点。 - Rune FS
在违规点记录的原因有两个。第一个是该方法可以发送对象及其值以进行记录。第二个是调用栈中可能存在更高层次的代码,最终会忽略或错误处理异常,并且不会被记录。 - Bob Horn
11个回答

19

不要捕获所有异常。异常会在堆栈中向上传递。您需要确保在异常到达堆栈顶部之前将其捕获。

这意味着,例如,您应该使用try/catch块来包围事件处理程序的代码。事件处理程序可能是“堆栈顶部”。对于ThreadStart处理程序或来自异步方法的回调也是如此。

您还想在层边界捕获异常,不过在这种情况下,您可能只需将异常包装在特定于该层的异常中。

在ASP.NET的情况下,您可以决定允许ASP.NET健康监视为您记录异常。

但是,您绝对不需要在每个方法中捕获异常。那是一个重大的反模式。如果您使用此类异常处理提交代码,我会强烈反对。


1
+1 for“不要抓取所有东西。”这是一个不错的开始,尽管我会说“不要,请不要抓取一切。” :) - 我发现当代码这样做时,它更难以跟踪。 - Mark Schultheiss
@JohnSaunders - 如果我们想记录导致异常的被调用函数中的某些参数,该怎么办?否则这些信息将会丢失。 - user3141326
1
那将是一个值得捕获、记录和重新抛出的原因。 - John Saunders
@JohnSaunders - 难道不可以使用编辑并继续功能在异常开始冒泡之前实际调试和修复代码吗?这就是我这样做的原因 - 提高生产力。 - MicroservicesOnDDD
@JohnSaunders - 这就是我想要 mixins 或多重继承或某种方式来保持所有这些垃圾不被干扰,除非我需要它,然后通过一种设置将 try-catch(all)-rethrow 暂时放在所有东西上(通过某种代码生成的方式)。那就是我想象中的。目前我正在尝试弄清楚 vb.net 的异常处理,我最近才开始使用它,感到很困惑。 - MicroservicesOnDDD

9

您可以在堆栈跟踪中查看所有内容 - 无需尝试/捕获每个方法。

遵循以下几个规则:

  1. 仅在需要使用自定义异常类型时使用try/catch
  2. 仅在上层需要知道时定义新的异常类型
  3. 在顶层进行try/catch,而不是对每个方法进行操作

5

好的,我已经阅读了所有建议在顶层放置单个try/catch的答案,现在我想提出另一种观点。

我不会在每个方法中放置try/catch,远非如此。但是我在我预计会失败的代码段(例如打开文件)周围使用try/catch,并且我希望添加附加信息到异常中(以便在更高层次记录日志)。

堆栈跟踪和消息“权限被拒绝”可能足以让您作为程序员找出问题所在,但我的目标是向用户提供有意义的信息,例如“无法打开文件'C:\ lockedfile.txt'。 权限被拒绝。”。

如下:

private void DoSomethingWithFile(string filename)
{
    // Note: try/catch doesn't need to surround the whole method...

    if (File.Exists(filename))
    {
        try
        {
            // do something involving the file
        }
        catch (Exception ex)
        {
            throw new ApplicationException(string.Format("Cannot do something with file '{0}'.", filename), ex);
        }
    }
}

我想提醒一下,即使有人说“只使用一个try/catch”,他们很可能仍会在代码中使用try/finally,因为这是确保正确清理等操作的唯一方法。

2
FYI,“ApplicationException”已被弃用。我也不会捕获“Exception”,而是只捕获与我在“try”块中所做的相关异常,比如“IOException”。 - John Saunders
通常情况下,只有在不捕获异常会更糟糕的情况下才捕获异常。 - John Saunders

2

我绝对不会在每个方法周围使用try-catch包装器(有趣的是,我在刚开始时确实这样做了,但那是在我学到更好的方法之前)。

1)为了防止程序崩溃和用户丢失信息,我这样做:

                runProgram:
                try
                {
                    container.ShowDialog();
                }
                catch (Exception ex)
                {
                    ExceptionManager.Publish(ex);
                    if (MessageBox.Show("A fatal error has occurred.  Please save work and restart program.  Would you like to try to continue?", "Fatal Error", MessageBoxButtons.YesNo) == DialogResult.Yes)
                        goto runProgram;
                    container.Close();
                }

容器是我的应用程序的起点,因此它基本上在我的整个应用程序周围放了一个包装器,以便没有任何东西会导致无法恢复的崩溃。这是我不介意使用goto的少数情况之一(它只有少量代码,仍然非常可读)。

2) 我只在我期望出现问题的方法(如超时)中捕获异常。

3) 为了提高可读性,如果您有一个try catch块,并且在try部分和catch部分都有一堆代码,最好将该代码提取到一个命名良好的方法中。

   public void delete(Page page) 
   {
      try 
      {
         deletePageAndAllReferences(page)
      }
      catch (Exception e) 
      {
         logError(e);
      }
   }

2
  1. 捕捉并重新抛出无法处理的异常只会浪费处理器时间。如果您对该异常无法做任何处理或无法解决它,请忽略它并让调用者响应它。
  2. 如果您想记录每个异常,全局异常处理程序就可以胜任。在.NET中,堆栈跟踪是一个对象;它的属性可以像任何其他对象一样被检查。您可以将堆栈跟踪的属性(甚至以字符串形式)写入您选择的日志中。
  3. 如果您想确保捕捉到每个异常,全局异常处理程序应该能够解决问题。事实上,没有任何应用程序可以没有全局异常处理程序。
  4. 您的catch块应该捕捉您知道可以从容地恢复的异常。也就是说,如果您可以处理它,请捕捉它。否则,让调用者去担心。如果没有任何调用者可以处理它,请让全局异常处理程序捕捉它并记录它。

为全局异常处理程序#3加1。 - MicroservicesOnDDD

1

要在发生的地方处理异常,你仍然需要使用 try/catch。但是你不一定需要在每个地方都捕获异常。它们会沿着调用栈向上传播,当它们被捕获时,你会得到一个堆栈跟踪。因此,如果出现问题,你可以随时根据需要添加更多的 try/catch。

考虑查看其中一个可用的日志记录框架。


在发生异常的地方使用try/catch的目的是确保它被记录。如果它传播,可能会出现其他异常和/或其他代码可能无法像我希望的那样记录它。 - Bob Horn
考虑查看其中一个可用的日志框架。 - Robert Harvey
他们可以提供关于最佳实践的见解。 - Robert Harvey

1

我认为你不需要在违规点立即捕获所有内容。你可以冒泡异常,然后使用 StackTrace 找出实际发生违规的地方。

此外,如果你需要有一个 try catch 块,最好的方法是将其隔离在一个方法中,以免用大量的 try catch 块来混淆代码。同时,尽可能少使用 try 语句。

当然,重申一下,将异常冒泡到顶部并记录 stacktrace 是比在代码中多次嵌套 try-catch-log-throw 块更好的方法。


1

我建议使用ELMAH来处理异常,这基本上是一个“让异常发生”的概念。 ELMAH会负责记录它们,并且您甚至可以将其设置为在某个项目的异常达到或超过特定阈值时向您发送电子邮件。 在我们的部门中,我们尽可能远离try/catch块。 如果应用程序出现问题,我们希望立即知道问题所在,以便我们可以修复它,而不是压制异常并在代码中处理它。

如果发生异常,那就意味着有些事情不对劲。 思想是使您的应用程序只执行应该执行的操作。 如果它正在执行不同的操作并导致异常,则您的响应应该是修复它发生的原因,而不是让它发生并在代码中处理它。 这只是我的/我们的哲学,不适用于所有人。 但是,我们已经被应用程序“吃掉”异常太多次了,因为某种原因或另一个原因,没有人知道出了什么问题。

永远不要捕获通用异常。始终捕获最具体的异常,这样如果抛出异常,但它不是您期望的类型,您将知道,因为应用程序将崩溃。如果您只是捕获(Exception e),那么无论抛出什么类型的异常,您的catch块现在都将负责响应可能抛出的每种类型的异常。如果它没有,那么您就会遇到整个“吞噬”异常的问题,其中某些东西出了问题,但直到很可能为时已晚才知道。


看起来ELMAH是一个仅限于ASP.NET的框架,因为它使用HTTP模块和处理程序去实现看起来非常好用的工具。MSDN上的一篇文章介绍了它——"Using HTTP Modules and Handlers to Create Pluggable ASP.NET Components" - MicroservicesOnDDD

0
如何避免在每个方法中使用try/catch,但仍然记录错误发生的位置?
这取决于托管环境。Asp.Net、WinForms和WPF都有不同的捕获未处理异常的方式。但是一旦全局处理程序传递了异常实例,您可以从异常中确定抛出点,因为每个异常都包括堆栈跟踪。

根据反馈,我认为现在我对这种方法感到舒适了:我的WCF服务仅在边界处(服务类本身,而不是在BLL或DAL层)包含一个try/catch。在边界处,我会记录日志并记录堆栈跟踪。然后我会抛出一个异常,以便客户端可以捕获并处理它。 - Bob Horn
关于WCF,需要记住的是网页服务使用故障。如果你要这样做,请选择一个单一的FaultContract,并抛出FaultException<thatContract>。你的顶层try/catch应该允许所有FaultException通过,因为另一层可能返回了特定的故障。 - John Saunders

0

现实情况是,避免细粒度的try/catch。让异常向上遍历堆栈,并在尽可能高的级别上被捕获。如果您有特定的关注点,则在立即catch中放置日志记录,如果您担心异常会级联 - 尽管您仍然可以通过深入内部异常来解决这些问题。

异常处理不应该是一个事后想法。确保您始终如一地执行它。我见过很多人从每个方法的开始到结束都放置了一个广泛的try/catch并捕获了一般异常。人们认为这可以帮助他们获得更多信息,但实际上并非如此。在某些情况下,更少才是更多。我永远不会厌倦“异常应用于标记异常行为”的格言。如果可以恢复,请尝试减少总体异常数量。当您尝试解决问题并在出现问题时看到数百个相同的NullReferenceException或类似异常时,没有什么比这更令人沮丧的了。


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