关于C#:如何使用Try-Catch进行异常处理是最佳实践

How using try catch for exception handling is best practice

在维护同事的代码时,即使是声称是高级开发人员的人,我也经常看到以下代码:

1
2
3
4
5
6
7
8
try
{
  //do something
}
catch
{
  //Do nothing
}

或者有时他们会将日志信息写入日志文件,如try catch块之后的日志文件。

1
2
3
4
5
6
7
8
try
{
  //do some work
}
catch(Exception exception)
{
   WriteException2LogFile(exception);
}

我只是想知道他们所做的是否是最佳实践?这让我困惑,因为在我看来,用户应该知道系统会发生什么。

请给我一些建议。


我的异常处理策略是:

  • 要通过挂接到Application.ThreadException event来捕获所有未处理的异常,然后决定:

    • 对于UI应用程序:向用户弹出道歉消息(winforms)
    • 对于服务或控制台应用程序:将其记录到文件(服务或控制台)

然后,我总是将外部运行在try/catch中的每段代码括起来:

  • WinForms基础结构触发的所有事件(加载、单击、选择的已挂起…)
  • 由第三方组件激发的所有事件

然后我附上"试/抓"

  • 我所知道的所有操作可能不会一直有效(IO操作、带零除法的计算…)。在这种情况下,我抛出一个新的ApplicationException("custom message", innerException)来跟踪实际发生的事情。

另外,我尽力对异常进行正确的排序。例外情况如下:

  • 需要立即向用户显示
  • 需要一些额外的处理来将发生的事情放在一起,以避免层叠问题(即:在TreeView填充期间在finally部分中放置endupdate)
  • 用户不在乎,但知道发生了什么很重要。所以我总是记录它们:

    • 在事件日志中
    • 或者在磁盘上的.log文件中

设计一些静态方法来处理应用程序顶级错误处理程序中的异常是一个很好的实践。

我还强迫自己尝试:

  • 请记住,所有异常都将冒泡到顶层。不需要在任何地方放置异常处理程序。
  • 可重用的或深度调用的函数不需要显示或记录异常:它们要么自动冒泡,要么在我的异常处理程序中用一些自定义消息重新引发。

所以最后:

坏的:

1
2
3
4
5
6
7
8
9
// DON'T DO THIS, ITS BAD
try
{
    ...
}
catch
{
   // only air...
}

无用的:

1
2
3
4
5
6
7
8
9
// DONT'T DO THIS, ITS USELESS
try
{
    ...
}
catch(Exception ex)
{
    throw ex;
}

在没有捕获的情况下进行最终尝试是完全有效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
    listView1.BeginUpdate();

    // If an exception occurs in the following code, then the finally will be executed
    // and the exception will be thrown
    ...
}
finally
{
    // I WANT THIS CODE TO RUN EVENTUALLY REGARDLESS AN EXCEPTION OCCURED OR NOT
    listView1.EndUpdate();
}

我在最高层的工作是:

1
2
3
4
5
6
7
8
9
10
11
12
13
// i.e When the user clicks on a button
try
{
    ...
}
catch(Exception ex)
{
    ex.Log(); // Log exception

    -- OR --

    ex.Log().Display(); // Log exception, then show it to the user with apologies...
}

我在一些所谓的函数中所做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Calculation module
try
{
    ...
}
catch(Exception ex)
{
    // Add useful information to the exception
    throw new ApplicationException("Something wrong happened in the calculation module :", ex);
}

// IO module
try
{
    ...
}
catch(Exception ex)
{
    throw new ApplicationException(string.Format("I cannot write the file {0} to {1}", fileName, directoryName), ex);
}

对于异常处理(自定义异常)有很多事情要做,但是我试图记住的那些规则对于我所做的简单应用程序来说已经足够了。

下面是一个扩展方法的例子,以一种舒适的方式处理捕获的异常。它们是以一种可以链接在一起的方式实现的,并且很容易添加您自己捕获的异常处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Usage:

try
{
    // boom
}
catch(Exception ex)
{
    // Only log exception
    ex.Log();

    -- OR --

    // Only display exception
    ex.Display();

    -- OR --

    // Log, then display exception
    ex.Log().Display();

    -- OR --

    // Add some user-friendly message to an exception
    new ApplicationException("Unable to calculate !", ex).Log().Display();
}

// Extension methods

internal static Exception Log(this Exception ex)
{
    File.AppendAllText("CaughtExceptions" + DateTime.Now.ToString("yyyy-MM-dd") +".log", DateTime.Now.ToString("HH:mm:ss") +":" + ex.Message +"
"
+ ex.ToString() +"
"
);
    return ex;
}

internal static Exception Display(this Exception ex, string msg = null, MessageBoxImage img = MessageBoxImage.Error)
{
    MessageBox.Show(msg ?? ex.Message,"", MessageBoxButton.OK, img);
    return ex;
}


最佳实践是异常处理永远不应该隐藏问题。这意味着try-catch块应该非常罕见。

有3种情况下使用try-catch是有意义的。

  • 总是尽可能地处理已知的异常。但是,如果您期望有一个异常,通常最好先测试它。例如,解析、格式化和算术异常几乎总是首先由逻辑检查来处理,而不是由特定的try-catch来处理。

  • 如果需要对异常(例如日志记录或回滚事务)执行某些操作,则重新引发异常。

  • 总是尽可能高的处理未知异常——唯一应该使用异常而不重新抛出异常的代码应该是UI或公共API。

  • 假设您正在连接到一个远程API,这里您知道预期会出现某些错误(并且在这种情况下会出现一些错误),所以情况是1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try
    {
        remoteApi.Connect()
    }
    catch(ApiConnectionSecurityException ex)
    {
        // User's security details have expired
        return false;
    }

    return true;

    请注意,不会捕获其他异常,因为它们不是预期的。

    现在假设您正在尝试将某些内容保存到数据库中。如果失败,我们必须将其回滚,因此我们有案例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    try
    {
        DBConnection.Save();
    }
    catch
    {
        // Roll back the DB changes so they aren't corrupted on ANY exception
        DBConnection.Rollback();

        // Re-throw the exception, it's critical that the user knows that it failed to save
        throw;
    }

    注意,我们重新抛出了异常——上面的代码仍然需要知道有什么失败了。

    最后,我们有了UI——这里我们不想有完全未处理的异常,但是我们也不想隐藏它们。这里我们有一个案例3的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    try
    {
        // Do something
    }
    catch(Exception ex)
    {
        // Log exception for developers
        WriteException2LogFile(ex);

        // Display message to users
        DisplayWarningBox("An error has occurred, please contact support!");
    }

    但是,大多数API或UI框架都有执行案例3的通用方法。例如,ASP.NET有一个黄色的错误屏幕,它转储异常详细信息,但可以在生产环境中用更通用的消息替换。遵循这些是最佳实践,因为它可以节省大量代码,而且还因为错误记录和显示应该是配置决策而不是硬编码。

    这意味着案例1(已知的异常)和案例3(一次性UI处理)都有更好的模式(避免预期的错误或将错误处理交给UI)。

    即使是案例2也可以用更好的模式替换,例如事务范围(回滚块期间未提交的任何事务的using块)使开发人员更难错误地获得最佳实践模式。

    例如,假设您有一个大型的ASP.NET应用程序。错误记录可以通过elmah进行,错误显示可以在本地提供信息性的ysod,并且在生产中是一条很好的本地消息。数据库连接都可以通过事务作用域和using块进行。你不需要一个单独的try-catch块。

    tl;dr:最佳实践实际上是根本不使用try-catch块。


    异常是阻塞错误。好的。

    首先,最佳实践应该是不要为任何类型的错误抛出异常,除非它是阻塞错误。好的。

    如果错误是阻塞的,那么抛出异常。一旦异常被抛出,就不需要隐藏它,因为它是异常的;让用户知道它(您应该将整个异常重新格式化为对用户在UI中有用的东西)。好的。

    作为软件开发人员,您的工作是努力防止出现某些参数或运行时情况可能以异常结束的异常情况。也就是说,不能将异常设为静音,但必须避免这些异常。好的。

    例如,如果您知道某些整数输入可能带有无效的格式,请使用int.TryParse而不是int.Parse。在很多情况下,您可以这样做,而不只是说"如果失败了,只需抛出一个异常"。好的。

    抛出异常是昂贵的。好的。

    毕竟,如果抛出了异常,而不是在抛出异常后将其写入日志,那么最佳实践之一就是在第一次机会异常处理程序中捕获它。例如:好的。

    • ASP.NET:global.asax应用程序u错误
    • 其他:AppDomain.FirstChanceException事件。

    我的立场是,本地尝试/捕获更适合处理特殊情况,在这种情况下,您可以将异常转换为另一个异常,或者在非常、非常、非常、非常、非常特殊的情况下"静音"异常(库错误抛出了一个不相关的异常,您需要静音才能解决整个错误)。好的。

    对于其他案例:好的。

    • 尽量避免例外。
    • 如果这不可能:第一次机会异常处理程序。
    • 或者使用PostSharp工具(AOP)。

    回答@thewhiteambit的一些评论…

    @他们说:好的。

    Exceptions are not Fatal-Errors, they are Exceptions! Sometimes they
    are not even Errors, but to consider them Fatal-Errors is completely
    false understanding of what Exceptions are.

    Ok.

    首先,一个异常怎么可能是一个错误呢?好的。

    • 没有数据库连接=>异常。
    • 要分析到某个类型的字符串格式无效=>异常
    • 尝试解析JSON,而输入实际上不是JSON=>异常
    • 参数nullwhile object was expected=>exception
    • 某些库有错误=>引发意外的异常
    • 有一个插座连接,它会断开。然后尝试发送消息=>异常

    我们可能会列出引发异常的1K案例,毕竟,任何可能的案例都是错误的。好的。

    异常是一个错误,因为在一天结束的时候,它是一个收集诊断信息的对象——它有一条消息,并且在发生错误时发生。好的。

    没有例外的情况,没有人会抛出例外。异常应该阻塞错误,因为一旦抛出了异常,如果不尝试进入使用Try/Catch和实现控制流的异常,则意味着应用程序/服务将停止进入异常情况的操作。好的。

    另外,我建议每个人都检查一下马丁·福勒(JimShore)出版的fail fast范例。这就是我一直理解如何处理异常的方式,即使在一段时间前我还没有进入这个文档。好的。

    [...] consider them Fatal-Errors is completely false understanding of what exceptions are.

    Ok.

    通常情况下,异常会切断一些操作流程,并被处理为将它们转换为人类可理解的错误。因此,似乎异常实际上是一个更好的范例,可以处理错误案例并对其进行处理,以避免应用程序/服务完全崩溃,并通知用户/消费者出了问题。好的。关于@thewhiteambit关注的更多答案

    For example in case of a missing Database-Connection the program could
    exceptionally continue with writing to a local file and send the
    changes to the Database once it is available again. Your invalid
    String-To-Number casting could be tried to parse again with
    language-local interpretation on Exception, like as you try default
    English language to Parse("1,5") fails and you try it with German
    interpretation again which is completely fine because we use comma
    instead of point as separator. You see these Exceptions must not even
    be blocking, they only need some Exception-handling.

    Ok.

  • 如果您的应用程序可能在不将数据持久化到数据库的情况下脱机工作,则不应使用异常,因为使用try/catch实现控制流被认为是一种反模式。脱机工作是一个可能的用例,因此您可以实现控制流来检查数据库是否可访问,而不必等到数据库无法访问时再进行。好的。

  • 解析事件也是一个预期的情况(不是异常情况)。如果您希望这样做,那么就不要使用异常来控制流!.您从用户那里获得一些元数据来了解他/她的文化,并为此使用格式化程序!.NET也支持此环境和其他环境,这是一个例外,因为如果您希望应用程序/服务具有特定的区域性用法,则必须避免使用数字格式。好的。

  • An unhandled Exception usually becomes an Error, but Exceptions itself
    are not codeproject.com/Articles/15921/Not-All-Exceptions-Are-Errors

    Ok.

    本文只是作者的一个观点或观点。好的。

    由于维基百科也可能只是Article作者的观点,我不会说这是教条,而是检查一下例外编码的文章在某些段落的某个地方说了什么:好的。

    [...] Using these exceptions to handle specific errors that arise to
    continue the program is called coding by exception. This anti-pattern can quickly degrade software in performance and maintainability.

    Ok.

    它还指出:好的。

    异常用法不正确好的。

    Often coding by exception can lead to further issues in the software
    with incorrect exception usage. In addition to using exception
    handling for a unique problem, incorrect exception usage takes this
    further by executing code even after the exception is raised. This
    poor programming method resembles the goto method in many software
    languages but only occurs after a problem in the software is detected.

    Ok.

    老实说,我认为不能开发软件,不要认真对待用例。如果你知道…好的。

    • 数据库可以脱机…
    • 某些文件可以被锁定…
    • 某些格式可能不受支持…
    • 某些域验证可能失败…
    • 您的应用程序应在脱机模式下工作…
    • 不管用例是什么…

    …你不会用例外。您将使用常规的控制流来支持这些用例。好的。

    如果没有覆盖一些意外的用例,您的代码将很快失败,因为它将抛出一个异常。是的,因为例外是例外。好的。

    另一方面,最后,有时您会覆盖抛出预期异常的异常情况,但您不会抛出它们来实现控制流。这样做是因为您希望通知上层您不支持某些用例,或者您的代码无法处理某些给定的参数或环境数据/属性。好的。好啊。


    我知道这是一个古老的问题,但是这里没有人提到过这个msdn文章,而且是文档为我清理了它,msdn有一个非常好的文档,当以下条件成立时,您应该捕获异常:

    • You have a good understanding of why the exception might be thrown, and you can implement a specific recovery, such as prompting the user to enter a new file name when you catch a FileNotFoundException object.

    • You can create and throw a new, more specific exception.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int GetInt(int[] array, int index)
    {
        try
        {
            return array[index];
        }
        catch(System.IndexOutOfRangeException e)
        {
            throw new System.ArgumentOutOfRangeException(
               "Parameter index is out of range.");
        }
    }
    • You want to partially handle an exception before passing it on for additional handling. In the following example, a catch block is used to add an entry to an error log before re-throwing the exception.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        try
    {
        // Try to access a resource.
    }
    catch (System.UnauthorizedAccessException e)
    {
        // Call a custom error logging procedure.
        LogError(e);
        // Re-throw the error.
        throw;    
    }

    我建议阅读整个"异常和异常处理"部分,以及异常的最佳实践。


    您唯一应该让用户担心代码中发生了什么事情的时候,是他们是否可以或需要做一些事情来避免这个问题。如果他们可以更改表单上的数据,按下按钮或更改应用程序设置以避免出现问题,请通知他们。但用户无法避免的警告或错误只会让他们对您的产品失去信心。

    例外和日志是为您、开发人员而不是最终用户准备的。了解在捕获每个异常时要做的正确的事情远胜于仅仅应用一些黄金法则或依赖应用程序范围的安全网。

    无意识编码是唯一一种错误的编码方式。事实上,您觉得在这些情况下可以做一些更好的事情,这表明您投入到了良好的编码中,但是避免试图在这些情况下打上一些通用规则,并理解为什么要首先抛出一些东西,以及您可以做些什么来从中恢复。


    除此之外,我尝试以下方法:

    首先,我捕获特殊类型的异常,如除数为零、IO操作等等,并根据这些异常编写代码。例如,除数为零,这取决于值的来源,我可以提醒用户(例如,中间计算中的简单计算器(不是参数)到达除数为零),或者静默处理该异常,将其记录下来并继续处理。

    然后我尝试捕获剩余的异常并记录它们。如果可能,允许执行代码,否则警告用户发生了错误,并要求他们发送错误报告。

    在代码中,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    try{
        //Some code here
    }
    catch(DivideByZeroException dz){
        AlerUserDivideByZerohappened();
    }
    catch(Exception e){
        treatGeneralException(e);
    }
    finally{
        //if a IO operation here i close the hanging handlers for example
    }


    更好的方法是第二种方法(指定异常类型的方法)。这样做的好处是,您知道这种类型的异常可能发生在您的代码中。您正在处理此类异常,可以继续。如果出现任何其他异常,那么这意味着出现了错误,这将帮助您在代码中查找错误。应用程序最终会崩溃,但您会发现您遗漏了一些需要修复的错误。


    第二种方法是很好的。

    如果您不想通过显示与应用程序用户无关的运行时异常(即错误)来显示错误并混淆应用程序用户,那么只需记录错误,技术团队就可以查找并解决问题。

    1
    2
    3
    4
    5
    6
    7
    8
    try
    {
      //do some work
    }
    catch(Exception exception)
    {
       WriteException2LogFile(exception);//it will write the or log the error in a text file
    }

    我建议您对整个应用程序使用第二种方法。


    对于例外情况,您应该考虑这些设计指南

    • 异常的性能及抛出
    • 使用标准异常类型
    • 例外和履行

    https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/exceptions/例外


    没有任何论据的catch只是吃了这个例外,没有任何用处。如果发生致命错误怎么办?如果你不用争论就使用catch,就不可能知道发生了什么。

    catch语句应该捕获更具体的异常,如FileNotFoundException,然后在最后捕获Exception,它将捕获任何其他异常并记录它们。


    对我来说,处理异常可以看作是业务规则。显然,第一种方法是不可接受的。第二个是更好的,如果上下文这么说,它可能是100%正确的方式。例如,现在您正在开发一个Outlook加载项。如果外接程序引发未处理的异常,则Outlook用户现在可能知道,因为一个插件失败,Outlook不会自行销毁。你很难找出问题所在。因此,在这种情况下,第二种方法对我来说是正确的。除了记录异常之外,您还可以决定向用户显示错误消息——我将其视为业务规则。


    有时您需要处理对用户没有任何意义的异常。

    我的方法是:

    • 在应用程序级别(即global.asax中)捕获未捕获的异常,以获取关键异常(应用程序不可用)。我对这个地方不感兴趣。只需将它们登录到应用程序级别,让系统完成它的工作。
    • 捕获"就地"并向用户显示一些有用的信息(输入了错误的数字,无法解析)。
    • 在适当的地方,不要对诸如"我将在后台检查更新信息,但是服务没有运行"之类的边缘问题做任何操作。

    它绝对不必是最佳实践。;-)


    留空catch块是最糟糕的事情。如果出现错误,最好的处理方法是:

  • 将其登录到文件数据库等。
  • 试着在飞行中修复它(也许试着用另一种方法来做那个操作)
  • 如果我们无法修复,请通知用户存在一些错误,当然要中止操作。

  • 最佳实践是在错误发生时抛出异常。因为发生了错误,不应该隐藏它。

    但在现实生活中,当你想隐藏这个的时候,你可以有几种情况

  • 您依赖第三方组件,希望在出现错误时继续该程序。
  • 您有一个业务案例需要在出现错误时继续进行