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

在维护我同事的代码时,甚至有人声称自己是高级开发人员,我经常看到以下代码:

try
{
//do something
}
catch
{
//Do nothing
}

或者有时他们把日志信息写到日志文件中,比如下面的try catch

try
{
//do some work
}
catch(Exception exception)
{
WriteException2LogFile(exception);
}

我只是想知道他们所做的是不是最好的做法?这让我很困惑,因为在我看来,用户应该知道系统发生了什么。

348807 次浏览

第二种方法很好。

如果你不想显示错误,并通过显示运行时异常(即。错误),这与他们无关,然后只记录错误,技术团队可以寻找问题并解决它。

try
{
//do some work
}
catch(Exception exception)
{
WriteException2LogFile(exception);//it will write the or log the error in a text file
}

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

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

但在现实生活中,你可以有几种情况想要隐藏它

  1. 您依赖于第三方组件,并且希望在出现错误时继续执行程序。
  2. 您有一个业务案例,需要继续进行,以防出现错误

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

  1. 登录到文件数据库等。
  2. 尝试在飞行中修复它(可能尝试做该操作的替代方式)
  3. 如果我们不能修复这个问题,那么就通知用户有错误,当然还要中止操作

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

有时需要处理与用户无关的异常。

我的方法是:

  • 在应用程序级别捕获未捕获的异常。在global.asax中)用于关键异常(应用程序可能没有用处)。这些例外情况我不太了解。只要在应用程序级别上登录它们,让系统完成它的工作。
  • 捕捉“on place”并向用户显示一些有用的信息(输入错误的数字,无法解析)。
  • 抓住位置,对边缘问题什么都不做,比如“我将在后台检查更新信息,但服务没有运行”。

它绝对不需要成为最佳实践。: -)

对我来说,处理异常可以看作是业务规则。显然,第一种方法是不可接受的。第二种是更好的,如果上下文这么说,它可能是100%正确的。例如,现在您正在开发一个Outlook插件。如果你的插件抛出了未处理的异常,outlook用户现在可能知道它,因为outlook不会因为一个插件失败而破坏自己。你很难找出哪里出了问题。因此,在这种情况下,第二种方法对我来说是正确的。除了记录异常,您还可以决定将错误消息显示给用户-我认为这是一个业务规则。

异常是屏蔽错误

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

如果错误是阻塞,则抛出异常。一旦异常已经被抛出,就没有必要隐藏它,因为它是异常的;让用户知道它(你应该在UI中将整个异常重新格式化为对用户有用的内容)。

作为软件开发人员,你的工作是努力防止特殊情况,其中一些参数或运行时情况可能以异常结束。即例外情况不能被压制,但必须避免

例如,如果你知道某些整数输入可能带有无效的格式,则使用int.TryParse而不是int.Parse。在很多情况下,你可以这样做,而不是仅仅说“如果它失败了,就抛出一个异常”。

抛出异常的代价很高。

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

  • ASP。NET: 全球。asax Application_Error
  • 其他:应用程序域中。FirstChanceException事件

我的立场是,局部try/catch更适合处理特殊情况,例如您可能会将一个异常转换为另一个异常,或者当您想要“静音”一个非常非常非常非常非常特殊的情况(库错误抛出一个不相关的异常,您需要静音以解决整个错误)。

其余情况:

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

回复@thewhiteambit的一些评论…

@thewhiteambit说:

异常不是致命错误,它们是异常!有时他们 甚至不是错误,但认为它们是致命错误是完全的

.错误理解异常

首先,异常怎么就不能是错误呢?

  • No database connection =>异常。
  • 解析为某些type =>异常的无效字符串格式
  • 试图解析JSON,而输入实际上不是JSON =>异常
  • 参数null while object was expected =>异常
  • 某些库有bug =>抛出意外异常
  • 有一个套接字连接,它被断开了。然后尝试发送message =>异常
  • ...

我们可以列出1k个抛出异常的情况,毕竟,任何可能的情况都将是一个错误

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

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

此外,我建议每个人都检查快速失败范例马丁·福勒出版(吉姆·肖尔撰写)。这就是我总是理解如何处理异常的方式,甚至在我开始研究这个文档之前。

[…致命错误是完全错误的理解什么是异常。

通常异常减少一些操作流程,他们被处理转换为人类可以理解的错误。因此,似乎异常实际上是处理错误案例和处理它们以避免应用程序/服务完全崩溃并通知用户/消费者出错的更好范例。

更多关于@thewhiteambit关注点的答案

例如,在缺少数据库连接的情况下,程序可以 异常情况下继续写入本地文件并发送 当数据库再次可用时,更改数据库。你的无效的 字符串到数字类型转换可以尝试再次解析 Exception上的language-local解释,就像你尝试默认一样 要解析的英语语言(“1,5”)失败了,你试着用德语 再解释一下,这完全没问题,因为我们用了逗号 而不是点作为分隔符。你看这些异常一定不均匀 如果阻塞,他们只需要一些异常处理
  1. 如果你的应用可能离线工作而不持久化数据到数据库,你不应该使用异常,因为使用try/catch实现控制流被认为是反模式。离线工作是一个可能的用例,所以你实现控制流来检查数据库是否可访问,而不是等到它不可访问。

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

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

这篇文章只是作者的一个观点或观点。

由于维基百科也可以只是文章作者的观点,我不会说它是这个法则,但检查异常编码文章在某些段落中的某处说:

< p >[…使用这些异常来处理出现的特定错误 继续这个程序被称为异常编码。这种反模式会迅速降低软件的性能和可维护性。 < / p >

它还在某处说:

异常使用不正确

通常,异常编码会导致软件中进一步的问题 使用不正确的异常。除了使用异常 处理一个独特的问题,不正确的异常使用 甚至在异常引发后执行代码。这 在许多软件中,糟糕的编程方法类似于goto方法

.语言,但仅在检测到软件中的问题后才会出现

老实说,我相信如果不认真对待用例,软件就无法开发。如果你知道…

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

……你不会为此使用异常。你可以使用常规的控制流支持这些用例。

如果一些意想不到的用例没有被覆盖,你的代码将很快失败,因为它会抛出一个异常。对,因为异常是特殊情况

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

没有任何参数的catch只是异常,没有任何用处。如果发生致命错误怎么办?如果不带参数地使用catch,就无法知道发生了什么。

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

我的异常处理策略是:

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

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

然后,我总是在try/catch中包含外部运行的每一段代码:

  • WinForms基础架构触发的所有事件(加载、单击、选定更改…)
  • 由第三方组件触发的所有事件

然后加上try/catch

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

此外,我尽力正确排序异常。但也有例外:

  • 需要立即显示给用户

  • 需要一些额外的处理来将事情放在一起,当它们发生时,以避免级联问题(即:在TreeView填充期间将. endenddate放在finally节中)

  • 用户并不关心,但重要的是知道发生了什么。所以我总是记录它们:

  • 在事件日志中

  • 或磁盘上的.log文件

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

我还强迫自己试着:

  • 记得所有异常都被冒泡到顶层。没有必要把异常处理程序放在任何地方。
  • 可重用的或深度调用的函数不需要显示或记录异常:它们要么自动生成,要么在异常处理程序中使用一些自定义消息重新抛出。

最后:

缺点:

// DON'T DO THIS; ITS BAD
try
{
...
}
catch
{
// only air...
}

用处:

// DON'T DO THIS; IT'S USELESS
try
{
...
}
catch(Exception ex)
{
throw ex;
}

have a try finally without a catch是完全正确的:

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 OCCURRED OR NOT
listView1.EndUpdate();
}

我在顶层做的是:

// 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...
}

我在一些被调用的函数中所做的:

// 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);
}

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

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

// 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;
}

对于异常,我尝试如下:

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

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

在代码中,是这样的:

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
}

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

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

  1. 总是处理已知异常尽可能低。但是,如果您预期会出现异常,那么通常更好的做法是先进行测试。例如,解析、格式化和算术异常总是先由逻辑检查来处理,而不是特定的try-catch

  2. 如果您需要对异常做一些事情(例如记录日志或回滚事务),则重新抛出异常。

  3. 总是尽可能高地处理unknown异常- only代码应该消耗异常而不重新抛出它,应该是UI或公共API。

假设你连接到一个远程API,在这里你知道预期某些错误(在这些情况下有一些事情),所以这是情况1:

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


return true;

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

现在假设您试图将一些东西保存到数据库中。如果失败,我们必须回滚它,所以我们有情况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;
}

注意,我们重新抛出了异常——更高级别的代码仍然需要知道某些事情失败了。

最后我们有了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!");
}

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

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

即使情况2也可以被更好的模式所取代,例如事务作用域 (using块,回滚在块期间未提交的任何事务)使开发人员更难获得最佳实践模式错误。

例如,假设你有一个大规模的ASP。网络应用程序。错误日志可以通过ELMAH来记录,错误显示可以是一个本地的有信息的YSoD,在生产中可以是一个很好的本地化消息。数据库连接都可以通过事务作用域和using块。你不需要一个单独的try-catch块。

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

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

异常和日志是为您,即开发人员,而不是最终用户准备的。当您捕捉到每个异常时,了解正确的做法要比仅仅应用一些黄金规则或依赖于应用程序范围的安全网要好得多。

盲目的编码是唯一一种错误的编码。事实上,您觉得在这些情况下可以做一些更好的事情,这表明您对良好的编码进行了投资,但避免试图在这些情况下确定一些通用规则,并首先了解抛出某些东西的原因,以及您可以做些什么来从中恢复。

我知道这是一个老问题,但这里没有人提到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.");
}
}
  • 您希望在将异常传递给其他处理之前对其进行部分处理。在下面的示例中,catch块用于在重新抛出异常之前向错误日志添加条目。
    try
{
// Try to access a resource.
}
catch (System.UnauthorizedAccessException e)
{
// Call a custom error logging procedure.
LogError(e);
// Re-throw the error.
throw;
}

我建议阅读完整的“异常和异常处理"section和异常的最佳实践

您应该考虑这些异常的设计指南

  • 异常抛出
  • 使用标准异常类型
  • 异常和性能

https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/exceptions

我可以告诉你:

代码片段#1是不可接受的,因为它忽略了异常。(它像什么都没发生一样吞下去)。

所以不要添加不做任何事情或只是重新抛出的catch块。

Catch块应该增加一些价值。例如,向最终用户输出消息或记录错误。

不要对正常的流程序逻辑使用异常。例如:

例如输入验证。这是无效的异常情况,你应该写方法IsValid(myInput);来检查输入项是否有效。

设计代码以避免异常。例如:

int Parse(string input);

如果我们传递一个不能解析为int的值,这个方法会抛出一个异常,我们可以这样写:

bool TryParse(string input,out int result); <-此方法将返回布尔值,指示解析是否成功。

也许这有点超出了这个问题的范围,但我希望这将帮助你在涉及try {} catch(){}和异常时做出正确的决定。