如何使用try catch进行异常处理是最佳实践

Toa*_*yen 198 .net c# exception-handling exception try-catch

在保持我同事的代码甚至是自称是高级开发人员的人的同时,我经常看到以下代码:

try
{
  //do something
}
catch
{
  //Do nothing
}
Run Code Online (Sandbox Code Playgroud)

或者有时他们将日志信息写入日志文件,如下面的try catch

try
{
  //do some work
}
catch(Exception exception)
{
   WriteException2LogFile(exception);
}
Run Code Online (Sandbox Code Playgroud)

我只是想知道他们所做的是最佳做法吗?这让我感到困惑,因为在我看来,用户应该知道系统会发生什么.

请给我一些建议.

Lar*_*rry 294

我的异常处理策略是:

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

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

然后,我始终将每一件在外部运行代码try/catch:

  • Winforms基础架构触发的所有事件(Load,Click,SelectedChanged ...)
  • 由第三方组件触发的所有事件

然后我附上'try/catch'

  • 我所知道的所有操作可能不会一直有效(IO操作,可能零分割的计算......).在这种情况下,我抛出一个新的ApplicationException("custom message", innerException)来跟踪真实情况

此外,我尽我所能正确排序异常.有例外情况:

  • 需要立即向用户显示
  • 需要一些额外的处理才能将事情放在一起,以避免出现级联问题(例如:finallyTreeView填充过程中将.EndUpdate放入部分)
  • 用户并不在意,但重要的是要知道发生了什么.所以我总是记录它们:

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

设计一些静态方法来处理应用程序顶级错误处理程序中的异常是一种很好的做法.

我也强迫自己尝试:

  • 请记住,所有异常都会冒泡到最高级别.没有必要在任何地方放置异常处理程序.
  • 可重用或深度调用的函数不需要显示或记录异常:它们要么自动冒泡,要么在我的异常处理程序中使用一些自定义消息重新抛出.

最后:

坏:

// DON'T DO THIS, ITS BAD
try
{
    ...
}
catch 
{
   // only air...
}
Run Code Online (Sandbox Code Playgroud)

无用:

// DONT'T DO THIS, ITS USELESS
try
{
    ...
}
catch(Exception ex)
{
    throw ex;
}
Run Code Online (Sandbox Code Playgroud)

最终尝试没有捕获是完全有效的:

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();
}
Run Code Online (Sandbox Code Playgroud)

我在顶层做的事情:

// 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...
}
Run Code Online (Sandbox Code Playgroud)

我在一些被称为函数中做了什么:

// 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);
}
Run Code Online (Sandbox Code Playgroud)

与异常处理(自定义异常)有很多关系,但我试图记住的规则足以满足我所做的简单应用程序.

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

// 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 + "\n" + ex.ToString() + "\n");
    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;
}
Run Code Online (Sandbox Code Playgroud)

  • `catch(Exception ex){throw ex; 在C#中,`*比冗余*更糟糕(无论你捕获的异常类型如何).要重新抛出,请使用`throw;`.对于前者,异常将看起来像是来自你的`throw ex`而后者,它将恰当地源于原始的`throw`语句. (94认同)
  • MSDN建议[不应该在代码中抛出ApplicationException异常](http://stackoverflow.com/a/32854414/1497596). (4认同)
  • 伟大而富有建设性的答案,最重要的是我喜欢这句话**Only air**:)感谢`Application.ThreadException`事件,我没有意识到这一点,非常有用. (3认同)
  • 为什么要挂钩`Application.ThreadException`事件_and_用`catch(Exception ex){ex.Log(ex);}`包装每个异常.我可能会同意前者是一种很好的做法,但后者会增加重复错误日志的风险并隐藏异常发生的风险.`throw ex`也非常糟糕. (2认同)

Kei*_*ith 61

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

有三种情况使用try-catch有意义.

  1. 始终尽可能低地处理已知异常.但是,如果您期望出现异常,通常最好先测试一下.例如,解析,格式化和算术异常几乎总是首先由逻辑检查更好地处理,而不是特定的try-catch.

  2. 如果您需要对异常执行某些操作(例如,记录或回滚事务),则重新抛出异常.

  3. 始终尽可能地处理未知异常 - 唯一应该使用异常而不是重新抛出异常的代码应该是UI或公共API.

假设您正在连接到远程API,在这里您可以预期会遇到某些错误(并且在这些情况下有所帮助),所以这是案例1:

try 
{
    remoteApi.Connect()
}
catch(ApiConnectionSecurityException ex) 
{
    // User's security details have expired
    return false;
}

return true;
Run Code Online (Sandbox Code Playgroud)

请注意,没有捕获其他异常,因为它们不是预期的.

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

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;
}
Run Code Online (Sandbox Code Playgroud)

请注意,我们重新抛出异常 - 更高的代码仍需要知道某些内容已失败.

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

try
{
    // Do something
}
catch(Exception ex) 
{
    // Log exception for developers
    WriteException2LogFile(ex);

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

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

这意味着案例1(已知异常)和案例3(一次性UI处理)都具有更好的模式(避免预期的错误或手动错误处理到UI).

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

例如,假设您有一个大规模的ASP.Net应用程序.错误记录可以通过ELMAH,错误显示可以是本地信息性的YSoD和生产中的一个很好的本地化消息.数据库连接都可以通过事务范围和using块进行.您不需要一个try-catch块.

TL; DR:最佳做法实际上是根本不使用try-catch块.

  • @Jorj你应该阅读整个帖子,如果你仍然不同意或许反对我的支持论点而不仅仅说你不喜欢我的结论会更有建设性.几乎总是比'try-catch'更好的模式 - 它可以(非常偶尔)有用,我不是在争论你永远不应该使用它们,但99%的时候有更好的方法. (4认同)

Mat*_*zer 33

阻塞错误是一个例外.

首先,最佳做法应该是不要为任何类型的错误抛出异常,除非它是阻塞错误.

如果错误被阻止,则抛出异常.一旦抛出异常,就没有必要隐藏它,因为它是特殊的; 让用户知道它(您应该将整个异常重新格式化为UI中对用户有用的东西).

您作为软件开发人员的工作是努力防止特殊情况,其中一些参数或运行时情况可能异常结束.也就是说,不能忽视异常,但必须避免这些异常.

例如,如果您知道某些整数输入可能带有无效格式,请使用int.TryParse而不是int.Parse.在很多情况下,您可以执行此操作,而不是仅仅说"如果失败,只需抛出异常".

抛出异常是昂贵的.

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

  • ASP.NET:Global.asax中的Application_Error
  • 其他:AppDomain.FirstChanceException事件.

我的立场是,本地尝试/捕获更适合处理特殊情况,您可以将异常转换为另一个异常,或者当您想要将其"静音"为一个非常非常非常非常特殊的情况时(库错误)抛出一个你需要静音的无关异常,以便解决整个bug).

对于其他案例:

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

回答@thewhiteambit的一些评论......

@thewhiteambit说:

例外不是致命错误,它们是例外!有时它们甚至不是错误,但要考虑它们,致命错误完全是错误理解异常是什么.

首先,异常怎么可能不是一个错误?

  • 没有数据库连接=>异常.
  • 要解析为某种类型=>异常的无效字符串格式
  • 尝试解析JSON,而输入实际上不是JSON => exception
  • null期望对象时的参数=>异常
  • 有些库有bug =>引发意外异常
  • 有一个套接字连接,它会断开连接.然后你尝试发送一个消息=>异常
  • ...

我们可能会列出抛出异常的1k个案例,毕竟,任何可能的情况都是错误的.

异常一个错误,因为在一天结束时它是一个收集诊断信息的对象 - 它有一条消息,它会在出现问题时发生.

在没有例外的情况下,没有人会抛出异常.例外情况应该是阻止错误,因为一旦它们被抛出,如果你不试图使用try/catch和exception来实现控制流,它们意味着你的应用程序/服务将停止进入特殊情况的操作.

另外,我建议大家检查Martin Fowler发表失败快速范式(由Jim Shore撰写).这就是我总是理解如何处理异常的方法,甚至在我不久之前得到这个文档之前.

[...]考虑他们致命错误完全错误地理解了什么例外.

通常例外会削减一些操作流程,并且处理它们以将它们转换为人类可理解的错误.因此,似乎异常实际上是一个更好的范例来处理错误情况并对它们进行处理以避免应用程序/服务完全崩溃并通知用户/消费者出错的地方.

关于@thewhiteambit关注的更多答案

例如,如果数据库连接丢失,程序可以异常地继续写入本地文件,并在数据库再次可用后将更改发送到数据库.您可以尝试使用Exception上的语言本地解释再次解析无效的String-To-Number转换,就像您尝试默认英语语言到Parse("1,5")失败并再次尝试德语解释一样很好,因为我们使用逗号而不是点作为分隔符.您看到这些异常甚至不能被阻止,它们只需要一些异常处理.

  1. 如果您的应用程序可能在不将数据持久保存到数据库的情况下脱机工作,则不应使用异常,因为实现控制流使用try/catch被视为反模式.脱机工作是一个可能的用例,因此您实现控制流来检查数据库是否可访问,您不要等到它无法访问.

  2. 分析事情也是预期的情况下(没有特殊情况).如果您期望这样,您不会使用异常来执行控制流程!.您可以从用户那里获得一些元数据,以了解他/她的文化,并为此使用格式化程序!.NET也支持这个和其他环境,也是一个例外,因为如果您期望对应用程序/服务进行特定于文化的使用,则必须避免使用数字格式.

未处理的异常通常会成为错误,但异常本身不是codeproject.com/Articles/15921/Not-All-Exceptions-Are-Errors

本文只是作者的观点或观点.

既然维基百科也可以只是关于作者的意见,我不会说它是教条,但是检查一下Coding by exception文章在某段中的某处说:

[...]使用这些异常来处理继续执行程序时出现的特定错误称为异常编码.这种反模式可以快速降低软件的性能和可维护性.

它也在某处说:

异常使用不正确

通常通过异常编码可能导致软件中的进一步问题,并且使用不正确的异常.除了对唯一问题使用异常处理之外,不正确的异常使用通过执行代码甚至在引发异常之后进一步采用这种方法.这种糟糕的编程方法类似于许多软件语言中的goto方法,但仅在检测到软件中的问题之后才发生.

老实说,我认为软件无法开发不认真对待用例.如果你知道......

  • 您的数据库可以脱机...
  • 某些文件可以锁定...
  • 某些格式可能不受支持...
  • 某些域验证可能会失败...
  • 您的应用应该在离线模式下工作...
  • 无论用例如何 ......

...... 你不会使用例外.您将使用常规控制流支持这些用例.

如果没有涵盖一些意外的用例,您的代码将快速失败,因为它会抛出异常.是的,因为异常是一种例外情况.

在另一方面,最后,有时你覆盖特殊情况下抛出预期的异常,但你不把它们实施控制流.这样做是因为您希望通知上层您不支持某些用例,或者您的代码无法使用某些给定的参数或环境数据/属性.


Chr*_*sCW 6

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

例外和日志适合您,开发人员,而不是您的最终用户.了解捕获每个异常时要做的正确事情远比仅仅应用一些黄金法则或依赖于应用程序范围的安全网要好得多.

无意识编码是唯一的错误编码.事实上,你觉得在这些情况下可以做得更好,这表明你投入了良好的编码,但是避免在这些情况下尝试标记一些通用规则并理解首先抛出某些东西的原因以及什么你可以做到从中恢复.


Ham*_*lla 5

我知道这是一个老问题,但在这里没有人提到的MSDN文章,它是真正清除它为我的文档,MSDN有一个很好的文件上,你应该捕捉异常,当满足以下条件:

  • 您可以很好地理解可能抛出异常的原因,并且可以实现特定的恢复,例如在捕获FileNotFoundException对象时提示用户输入新的文件名.

  • 您可以创建并抛出一个新的,更具体的异常.

int GetInt(int[] array, int index)
{
    try
    {
        return array[index];
    }
    catch(System.IndexOutOfRangeException e)
    {
        throw new System.ArgumentOutOfRangeException(
            "Parameter index is out of range.");
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 您希望在传递异常之前部分处理异常以进行其他处理.在以下示例中,catch块用于在重新抛出异常之前向错误日志添加条目.
    try
{
    // Try to access a resource.
}
catch (System.UnauthorizedAccessException e)
{
    // Call a custom error logging procedure.
    LogError(e);
    // Re-throw the error.
    throw;     
}
Run Code Online (Sandbox Code Playgroud)

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