为什么不捕获通用异常?

28

我的VS提示我:

警告2 CA1031:Microsoft.Design:修改“Program.Main(string[])”以捕获比“Exception”更具体的异常或重新抛出异常。

为什么我要这么做?如果我这样做,并且没有捕获所有异常来处理它们,我的程序会崩溃并显示一大堆错误信息。我不想让用户看到这种错误提示!

为什么我不应该一次性捕获所有异常,并显示一个友好的警告给用户,告诉他们:"出了点问题,不用担心,我会处理它,耐心等待就行"?

编辑:刚才发现我重复了,抱歉 链接

编辑2:澄清一下,任何异常被捕获后我都会退出程序!我只是不想让用户在控制台应用程序中发生未处理异常时看到那个弹出的"报告给微软"对话框!

12个回答

46

吞掉异常是一种危险的做法,因为:

  • 它会导致用户认为某些操作成功了,但实际上失败了。
  • 它可能会让你的应用程序处于你没有预料到的状态。
  • 它会使调试变得更加复杂,因为在处理奇怪/损坏的行为而不是一个堆栈跟踪时,很难找到故障发生的位置。

正如你可以想象的那样,其中一些后果可能是极其灾难性的,因此正确地处理异常是一种重要的习惯。

最佳实践

首先,进行防御性编码,使异常不必要地发生尽可能少。它们是计算密集型的。

在可能的情况下,以细粒度的方式处理预期的异常(例如:FileNotFoundException)。

对于意外的异常,您可以采取以下两种方法之一:

  • 让它们正常地上升并造成崩溃。
  • 捕获它们并优雅地失败。

优雅地失败?

假设您正在使用ASP.Net,并且不希望向用户显示黄色的死亡屏幕,但您也不希望问题被隐藏在开发团队之外。

在我们的应用程序中,我们通常在global.asax中捕获未处理的异常,然后进行日志记录和发送通知电子邮件。我们还会显示一个更友好的错误页面,可以使用customErrors标记在web.config中配置。

这是我们的最后一道防线,如果我们最终收到了电子邮件,我们会立即解决它。

这种模式并不与仅吞掉异常相同,在那种情况下,你有一个空的Catch块,只存在于“假装”异常没有发生。

其他说明

在VS2010中,有一种叫做Intellitrace的东西,可以让你将应用程序状态发送到电子邮件,然后逐步检查代码,在异常发生时检查变量值等。 这将非常有用。

1
不完全是,但观点很好。我从事基于控制台的应用程序开发,如果程序崩溃,我会非常讨厌 - 因此我捕获了所有异常并在控制台上打印出它们(带有堆栈跟踪),同时在后台记录它们。 - F.P
1
明白了。嗯,情况仍然相似——你可以向用户显示友好的消息,进行一些日志记录/报告,并正常退出(或重新启动/返回安全状态)。 - Brian MacKay
看看James Sheck的回答,那正是我所做的 - 虽然在我的问题中没有指出... - F.P
3
这条评论开头说“吞噬异常是一种危险的做法,因为......”,但我觉得这令人不安,因为原帖明确描述了以一种特定(可能不是最优)的方式处理通用异常,而不是吞噬它们。(我猜“吞噬异常”意味着“不做任何处理”)。我希望读到这篇(旧的)讨论的人注意到这种不一致。吞噬异常总是不好的。对于原帖提出的情况,答案应该更多地是“取决于......”。 - M.Bearden
@M.Bearden 您是正确的...我认为这个奇怪的答案是由于另一个回复的讨论引起的,然后之后有更新进一步细化了他的意思。实际上,这需要更多的更新。最后,我对即将推出的 VS2010 的惊人新功能感到惊叹。 :) 为了清楚地向任何阅读此文的人表明:吞噬异常是捕获异常然后完全忽略它们,就好像它们从未发生过一样。这可能会带来问题,而且只是 某种程度上 是 OP 所谈论的内容。 - Brian MacKay
显示剩余2条评论

16
因为程序会不加选择地捕获异常(然后继续运行),所以不能依赖它们执行预期的操作。这是因为您不知道被“忽略”的是哪种异常。如果出现了溢出或内存访问错误,导致财务账户中扣除了错误的金额该怎么办?如果它将船驶向冰山而不是远离它呢?意外的失败应始终导致应用程序终止。这迫使开发过程识别和纠正它发现的异常(演示期间崩溃是一个很好的激励因素),并且在生产中,在软件无法执行其设计任务时,允许适当设计的备份系统做出反应。
编辑:澄清用户界面组件和服务或中间件组件之间的区别。
在服务或中间件组件中,没有用户与代码组件在同一进程空间中交互,组件需要“传递”异常到当前正在处理调用的客户端组件。无论发生什么异常,组件都应尽可能地这样做。然而,在出现意外或未预料到的异常情况下,该组件应最终终止它正在运行的进程。对于已预期或预期的异常,应进行开发分析以确定是否对于特定的异常,该组件及其主机进程可以继续运行(处理未来的请求),还是应该被终止。

我曾经有一位教练坚持认为你应该捕获所有的异常,而我无法说服他这是个坏主意。不用说,在我的正常编程中我不会这样做。 - Powerlord
1
意外故障应始终导致应用程序终止。这在交互式应用程序中可能是可以接受的,但在许多情况下,放弃并退出是不合适的。您暗示了“适当设计的备份系统”,但没有限定条件,并且使用了“始终”。您可能需要进行限定/澄清。 - NVRAM
@NVRAM,是的,你说得对,我所说的话暗示了大量细节,但在这里回答问题只能放入有限的内容。尽管如此,冒着被绝对化的风险,我还是倾向于坚持我的“总是”描述。如果一个应用程序不能完成预期的任务(这是我对异常的定义),那么 - 你可以处理异常(那就处理),或者用户可以处理它(那就问问他/她),或者应用程序应该终止。 - Charles Bretana
1
某些答案,比如这个答案,在我看来似乎缺少了很多“它取决于”的东西。捕捉一般异常总是不好吗?不,这取决于情况。考虑一个简单的例子:一个长时间运行的服务器应用程序,启动了许多事务。处理单个事务的代码具有一系列特定异常类型的捕获,然后是一个最终的catch(Exception)块,记录所有可用的异常信息,声明单个事务失败,然后允许服务器启动下一个事务。这似乎是一个很好的想法,但违反了“总是总是总是错误”的模因。 - M.Bearden
不,它并没有...你的评论包含了线索。在“...声明单个事务失败...”的地方,你正在将异常“传播”到调用程序。如果在你的例子中,在单个事务的例程内部实际发生异常的地方,代码捕获并吞噬了它,那么你的代码“...声明单个事务失败...”就永远不会运行,对吗? - Charles Bretana
如果它将船驶向冰山而不是远离它怎么办?如果不吞咽异常会导致冰山检测程序崩溃,从而不允许船只远离冰山呢? - greg

7

你应该处理你能够处理的确切异常,让所有其他异常冒泡。如果它向用户显示一条消息,这意味着你不太清楚你能处理什么。


如果他的程序是main,那么难道就没有其他地方可以冒泡到吗? - Kathy Van Stone
@Kathy - 正确。在这种情况下,用户将看到不希望出现的对话框。问题是,程序应该识别它实际上可以处理的事情并这样做,但有些可能会使程序处于不稳定状态,应立即关闭。 - Otávio Décio

5

在应急救援人员使用的设备上工作过后,我宁愿用户看到一个丑陋的错误信息,也不愿意因为意外地吞咽了异常而误导用户认为一切“正常”。根据你的应用程序,其后果可能是什么都没有,失去销售机会,甚至是灾难性的生命损失。

如果一个人要捕捉所有异常,显示更好的错误对话框,然后退出应用程序,那没问题。但是如果他们在忽略未知异常后继续运行,我会因此解雇他们。这是不可以接受的。

良好的编程实践是假定人会犯错。假设已捕获和处理所有“重要”的异常是一个坏主意。


1
不,计划是显示一个漂亮的错误消息并在关键状态下退出程序(Environment.Exit(2))。 - F.P
没错,弗洛里安。你在原帖中清楚地表明了你正在考虑处理异常,就像你在这个评论中描述的那样。你从未提出过“吞噬”(默默地?)异常的话题……其他评论者提到了这个问题。 - M.Bearden

3
由于您的警告信息显示这是在Main()中,我假设在较低级别中,您只捕获更具体的异常。
对于Main(),我认为有两种情况:
1.您自己的(调试)构建,在这里您需要获取所有可能得到的异常信息:此时不要在此处捕获任何异常,以便调试器中断并显示您的调用堆栈。
2.您发布的公共版本,您希望应用程序正常运行:捕获异常并显示友好的消息。对于普通用户来说,这总是比'发送报告'窗口更好。
为了实现这一目标,请检查是否已定义DEBUG(如果VS没有自动执行,则定义它)。
#if DEBUG
  yadda(); // Check only specific Exception types here
#else
  try
  {
    yadda();
  }
  catch (Exception e)
  {
     ShowMessage(e); // Show friendly message to user
  }
#endif

我建议您禁用有关捕获常规异常的警告,但仅限于您的Main()函数。在任何其他方法中捕获异常是不明智的,正如其他帖子已经指出的那样。


这是一个非常好的想法,我一直喜欢将项目的开发和生产部分分开。不知道为什么我没有想到... - F.P
Lennaert:把条件指令具体放在“try {”和“} catch {... }”块周围,这样不是更好吗?这样可以避免重复代码。(当一个yadda()被更改而另一个没有被更改时,也可以避免引入错误。) - Ian Clelland
@Ian:这通常是我所做的,但我认为这样可以更好地说明这个想法。理想情况下,已经有多个catch块了,只有最后一个块(catch Exception)在#if/#endif中。 - Lennaert

3
简短回答:您应该修复您的错误。找到抛出异常的位置,除非它超出了您的控制-修复它。 同时,捕获(而不是重新抛出)各种异常会违反异常中立性。一般来说,您不希望这样做(尽管在main中捕获异常看起来像特殊情况)。

Apo Y2k:这个问题涉及最佳实践。作为作者,当然你可以自由地做任何你想做的事情,但是那么问问题的意义在哪里呢?在没有错误的程序中,用户将不会看到“错误消息”,因为异常被捕获并在适当的位置处理了。 - BostonLogan

2
有一种方法可以抑制代码分析中的某些消息。我曾经为了记录日志而使用过这个方法(捕获通用异常),效果还不错。当你添加这个属性时,它表明你至少已经意识到你是有特定原因打破了规则。对于不正确的catch块(除了记录日志之外的其他目的),你仍然会收到警告。 MSDN SuppressMessageAttribute

1
我完全赞成捕获特定已知异常并处理状态...但我使用一般的catch异常来快速定位问题并将错误传递给处理状态良好的调用方法。在开发过程中,一旦发现这些问题,它们就会与常规异常放在一起,并在发布时进行处理。
我认为应该在代码进入生产后尝试删除这些异常,但在初始代码创建期间不断受到干扰有点过分。
因此,在Microsoft.CodeQuality.Analyzers的项目设置中关闭(取消选中)警告。在项目设置下的代码分析中找到它。

enter image description here


1
所有答案都很好。但我想提出另一个选项。
作者想展示一些花哨的信息的意图是可以理解的。 此外,Windows默认的错误消息确实很丑陋。此外,如果应用程序未提交给“Windows卓越计划”,开发人员将不会收到有关此问题的信息。那么,如果默认运行时处理程序没有帮助,使用它有什么意义呢?
问题在于CLR主机的默认异常处理程序(https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2008/9x0wh2z3(v=vs.90)?redirectedfrom=MSDN)以非常安全的方式工作。它的目的很明确:记录错误、将其发送给开发人员、设置进程的返回代码并杀死它。更改默认处理程序的一般方法是编写自己的主机。在这种情况下,您可以提供自己的异常处理方式。
尽管如此,仍然有一个简单的解决方案,既满足CA1031,又满足大多数需求。
当捕获异常时,您可以按照自己的方式处理它(记录、显示消息等),最后可以设置进程结果代码并退出(例如使用Thread.Abort和“Exit”方法的混合)。但是,在catch块的末尾,您可以只放置“throw;”(由于ThreadAbortedException不会被调用,但将满足规则)。仍然有一些情况,比如StackOverflowException,无法像那样处理,您将看到默认消息框,为了修复它,您需要回退到自定义CLR主机选项。
此外,只是供您参考,您的应用程序可以运行多个线程(除了执行Main方法的线程之外)。要从所有线程接收异常,可以使用AppDomain.UnhandledException。此事件不允许您将异常“标记”为已处理,但您可以使用Thread.Join()冻结线程,然后使用另一个(更多)线程完成工作(记录、msgbox、退出)。
我理解这一切看起来有点棘手,可能不正确,但我们必须处理AppDomain.UnhandledException、ThreadAbortException、CorruptedState异常和默认CLR主机的实现。所有这些最终都不给我们留下太多选择。

0
在VS中,您可以设置自定义错误页面以在出现问题时向用户显示,而不是在try-catch中捕获它。我假设您正在使用ASP .NET,如果是这样,请在System.Web标记下添加此标记到您的Web.Config中:
<customErrors mode="RemoteOnly" defaultRedirect="~/CustomErrorPage.aspx" redirectMode="ResponseRewrite" />

你还可以在Global.asax文件中捕获所有未捕获的异常(如果你还没有它:右键单击Web项目,选择添加项,然后搜索它)。该文件中有许多应用程序范围的事件处理程序,比如“Application_Error”,它会捕获应用程序内部未捕获的每个异常,这样你就不必一直使用Try-Catch。如果发生异常,可以使用它来发送电子邮件给自己,并可能将其重定向到主页或其他地方,如果你不想使用上面的customErrors标记的话。

但是最终,你不希望将整个应用程序包装在try-catch中,也不希望捕获一个通用的异常。try-catch通常会减慢应用程序的运行速度,而且很多时候,如果你捕获了每个通用异常,可能要过上几个月或几年才能发现存在bug,因为try-catch导致你忽视了它。


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