捕获一般异常真的是一件坏事吗?


56

我通常同意大多数代码分析警告,并且我会坚持遵守。但是,我在这方面遇到了困难:

CA1031:不捕获常规异常类型

我了解此规则的理由。但是,在实践中,如果无论是否引发异常,我都想采取相同的措施,为什么我要专门处理每一个?此外,如果我处理特定的异常,如果我正在调用的代码发生更改以在将来引发新的异常,该怎么办?现在,我必须更改代码以处理该新异常。而如果我只是抓住了Exception我的代码就不必更改。

例如,如果Foo调用Bar,并且Foo不管Bar抛出的异常类型如何都需要停止处理,那么具体确定我要捕获的异常类型是否有任何优势?

也许是一个更好的例子:

public void Foo()
{
    // Some logic here.
    LogUtility.Log("some message");
}

public static void Log()
{
    try
    {
        // Actual logging here.
    }
    catch (Exception ex)
    {
        // Eat it. Logging failures shouldn't stop us from processing.
    }
}

如果您在此处未捕获到一般异常,则必须捕获所有可能的异常类型。帕特里克(Patrick)有一个优点,OutOfMemoryException不应以这种方式处理。那么,如果我想忽略所有例外情况OutOfMemoryException呢?


12
OutOfMemoryException呢 处理代码与其他所有代码一样吗?
Patrick

@Patrick好点。我在问题中添加了一个新示例来介绍您的问题。
Bob Horn

1
捕获一般异常与对每个人应该做什么应该做出一般性陈述一样糟糕。通常,没有一种万能的解决方案。

4
@Patrick that OutOfMemoryErroris,正是Exception由于这个原因,它与继承树是分开的
Darkhogg 2015年

键盘异常(例如Ctrl-Alt-Del)如何处理?
Mawg

Answers:


33

这些规则通常是一个好主意,因此应遵循。

但是请记住,这些是通用规则。它们不能涵盖所有情况。它们涵盖了最常见的情况。如果您有特定的情况,并且可以提出自己的技术更好的论据(并且应该能够在代码中写注释以阐明这样做的论据),则可以这样做(然后进行同行评审)。

在论点的反面。

我不认为您上面的示例是这样做的好机会。如果日志记录系统发生故障(大概记录了其他一些异常),那么我可能不希望该应用程序继续运行。退出并将异常打印到输出,以便用户可以看到发生了什么。


2
同意。在非常至少,现在,经常记录离线做出某种规定的那会给某些类型的调试输出,标准错误,如果没有别的。
Shadur 2012年

6
我赞成你们两个,但我认为您的想法像开发人员,而不是用户。只要该应用程序可用,用户通常就不会在乎是否在进行日志记录。
Mawg 2014年

1
有趣的是,由于log4net明确地不想仅因为记录失败而停止应用程序。我认为这取决于您所记录的内容;安全审核,当然可以终止该过程。但是调试日志?没那么重要。
安迪

1
@安迪:是的,一切都取决于你在做什么。
马丁·约克

2
你为什么不理解他们?而且您不应该努力这样做吗?
Mawg '16

31

是的,捕获一般异常是一件坏事。异常通常意味着程序无法执行您要求的操作。

可以处理几种类型的异常:

  • 致命的异常:内存不足,堆栈溢出等。一些超自然的力量刚刚弄乱了您的宇宙,进程已经死了。你无法使情况好转,所以放弃
  • 由于正在使用的代码中的错误而引发异常:不要尝试处理它们,而要解决问题的根源。不要将异常用于流量控制
  • 特殊情况:仅在这些情况下处理异常。这里可以包括:拔掉网络电缆,互联网连接停止工作,缺少权限等。

哦,这是一条一般规则:如果您不知道如何捕捉到异常,该怎么办,最好快速失败(将异常传递给调用方并让其处理)


1
问题中的具体情况如何?如果日志记录代码中存在错误,则可以忽略该异常,因为实际上重要的代码不应受到该异常的影响。
svick 2012年

4
为什么不修复日志记录代码中的错误?
Victor Hurdugaci,2012年

3
维克多,这可能不是错误。如果日志所在的磁盘空间不足,那是日志代码中的错误吗?
Carson63000

4
@ Carson63000-好,是的。未写入日志,因此:bug。实现固定大小的旋转日志。
不错,2012年

5
这是imo的最佳答案,但它缺少一个关键方面:安全性。由于坏人试图破坏您的程序,因此会发生一些异常。如果抛出了您无法处理的异常,那么您就不必再信任该代码了。没有人喜欢听,他们写道,让坏人中的代码。
riwalk

15

最顶层的外部循环应使用其中的一个来打印所有可能的内容,然后导致可怕的,暴力的和嘈杂的死亡(因为这不应该发生,需要有人听到)。

否则,您通常应该非常小心,因为您很可能没有预料到该位置可能发生的所有事情,因此很可能无法正确对待它。尽可能具体一些,这样您就只能捕获那些您知道会发生的事件,并让那些之前未曾看到的事件冒出来,引起上述嘈杂的死亡。


1
我在理论上同意这一点。但是我上面的日志记录示例呢?如果您只是想忽略日志记录异常并继续,该怎么办?您是否应该捕获所有可能引发的异常并处理每个异常,同时让更严重的异常冒出来呢?
鲍勃·霍恩

6
@BobHorn:有两种看待方式。是的,您可以说日志记录并不是特别重要,如果日志记录失败,则不应停止您的应用程序。这已经足够公平了。但是,如果您的应用程序无法登录五个月又不知道怎么办?我已经看到了这种情况。然后我们需要日志。灾害。我建议尽您所能想到的一切来阻止日志记录失败,而不是忽略它。(但您不会对此达成共识。)
pdr

@pdr在我的日志记录示例中,通常会发送电子邮件警报。或者,我们有用于监视日志的系统,如果未积极填充日志,我们的支持团队将收到警报。因此,我们将以其他方式意识到日志记录问题。
鲍勃·霍恩

@BobHorn您的日志记录示例相当虚构-我所知道的大多数日志记录框架都竭尽全力引发任何异常并且不会像这样被调用(因为这会使任何“日志语句在文件Y的X行发生”都没有用) )

@pdr我已经在登录列表(Java)上提出了一个问题,即如果日志记录配置失败,则如何使日志记录框架停止应用程序,这仅仅是因为拥有日志非常重要,并且任何静默失败都是不可接受的。一个好的解决方案仍在等待中。

9

这并不是说不好,而是特定的捕获效果更好。 具体来说,这意味着您实际上更具体地了解您的应用程序在做什么,并对它有更多的控制权。 通常,如果遇到仅捕获异常,将其记录并继续的情况,那么可能总会有一些不好的事情发生。 如果您专门捕获已知的代码块或方法可能引发的异常,那么实际恢复的可能性就更高,而不仅仅是记录并希望获得最佳结果。


5

两种可能性并不互斥。

在理想情况下,您将捕获方法可能生成的所有可能类型的异常,并根据每个异常进行处理,最后添加通用catch子句以捕获任何将来或未知的异常。这样,您可以两全其美。

try
{
    this.Foo();
}
catch (BarException ex)
{
    Console.WriteLine("Foo has barred!");
}
catch (BazException ex)
{
    Console.WriteLine("Foo has bazzed!");
}
catch (Exception ex)
{
    Console.WriteLine("Unknown exception on Foo!");
    throw;
}

请记住,为了捕获更具体的异常,必须将它们放在第一位。

编辑:出于评论中所述的原因,在上次捕获中添加了重新抛出。


2
等待。您为什么要记录未知的异常以进行控制台并继续进行?如果您甚至不知道该异常可能被代码抛出,那么您可能是在谈论系统故障。在这种情况下继续进行可能不是一个好主意。
pdr 2012年

1
@pdr当然,您意识到这是一个人为的示例。关键是要说明对特定和通用异常的捕获,而不是如何处理它们。
Rotem 2012年

@Rotem我认为您在这里有很好的答案。但是Pdr有一点。照原样,代码使捕获和继续看起来不错。一些初学者,看到此示例,可能会认为这是一个好方法。
鲍勃·霍恩

人为与否,这会使您的答案错误。一旦开始捕获“内存不足”和“磁盘已满”之类的异常并吞下它们,您就可以证明为什么捕获一般异常实际上是不好的。
pdr 2012年

1
如果仅捕获一般例外而将其记录下来,则应在记录后将其重新抛出。
Victor Hurdugaci 2012年

5

我最近一直在思考相同的问题,我的初步结论是,仅出现问题是因为.NET Exception层次结构被严重搞砸了。

举个例子来说,贫贱ArgumentNullException可能是你的一个例外,一个合理的候选人希望赶,因为它往往以指示出代码,而不是一个合法的运行时错误的错误。是啊。所以呢NullReferenceException,除了NullReferenceException不会从派生SystemException直接,所以没有斗,你可以把所有的“逻辑错误”为赶上(或抓不住)。

然后还有就是IMNSHO 主要具有糟蹋SEHException派生(通过ExternalExceptionSystemException,从而使它成为一个“正常”SystemException当你得到的SEHException,你想要写转储,并以最快的速度结束,你可以-而且开始.NET 4中至少一些 SEH异常被视为不会捕获的损坏状态异常。一件好事,而且使该CA1031规则变得更加无用的事实,因为现在您的懒惰catch (Exception)者无论如何都不会遇到这些最糟糕的类型。

然后,似乎其他Framework东西都不一致地Exception直接通过via 派生SystemException,试图通过诸如严重程度模拟之类的方式来对子句进行分组。

利珀特先生C#成名的作品叫做Vexing Exception,他在其中列出了一些有用的异常分类:您可能会争辩说,您只想捕获“外来的”异常,但.... C#NET的语言和设计除外框架异常使得不可能以任何简洁的方式“仅捕获外来的”。(并且,例如,OutOfMemoryException可以很好地是用于一个API一个完全正常的可恢复错误,其具有分配是在某种程度上较大的缓冲区)

对于我来说,最重要的C#CA1031是,catch块的工作方式和Framework异常层次结构的设计方式,规则是完全没有用的。它假装帮助解决“请勿吞咽异常”这一根本问题,但吞咽异常与捕获的内容零相关,而与您随后的处理无关:

至少 4种方式合法地处理陷入Exception,并且CA1031似乎只肤浅地处理它们中的一个(即重新掷情况下)。


附带说明一下,有一个名为Exception Filters的C#6功能,它将使它CA1031再次变得更有效,因为这样您就可以正确地,正确地,正确地过滤要捕获的异常,并且没有理由编写未过滤的catch (Exception)


1
一个主要的问题OutOfMemoryException是,没有一种很好的方法可以确保代码“仅”表明一个特定的分配失败,而该分配已经准备失败。有可能引发了其他一些更严重的异常,并且OutOfMemoryException在从该其他异常展开堆栈时发生了这种情况。Java可能凭借其“ try-with-resources”而迟到了,但是它在堆栈展开期间处理异常的能力比.NET更好。
2015年

1
@supercat-的确如此,但是在任何finally块中发生问题时,任何其他常规System异常几乎都是如此。
马丁·巴

1
的确是这样,但是一个人可以(并且通常应该)finally阻止那些实际上可以预期会以原始和新异常都被记录的方式发生的异常。不幸的是,这样做通常需要分配内存,这可能会导致自身故障。
2015年

4

口袋妖怪异常处理(一定要抓住一切!)当然并不总是不好的。在向客户(尤其是最终用户)公开方法时,通常最好捕获所有内容,而不是使应用程序崩溃或烧毁。

通常,尽管应该尽可能避免它们。除非您可以根据异常的类型采取特定的措施,否则最好不要处理它并让异常冒出来,而不是吞下该异常或不正确地处理它。

看看这个 SO答案以获得更多阅读。


1
这种情况(开发人员必须是Pokemon Trainer时)是在您自己制作整个软件时出现的:您制作了图层,数据库连接和用户的GUI。您的应用必须从连接断开,用户文件夹删除,数据损坏等情况中恢复。最终用户不喜欢崩溃和刻录的应用。他们喜欢可以在爆炸中崩溃的计算机上运行的应用程序!
Broken_Window 2014年

直到现在我还没有想到这一点,但是在《口袋妖怪》中,通常假设通过“全部捕获”,您需要每个捕获一个,并专门处理捕获,这与该短语的常用用法相反在程序员之间。
Magus 2014年

2
@Magus:使用类似的方法LoadDocument(),基本上不可能确定所有可能出错的地方,但是可能引发的异常中的99%只是意味着“无法将具有给定名称的文件内容解释为文件;处理它。” 如果有人试图打开不是有效文档文件的文件,则不应使应用程序崩溃并杀死任何其他打开的文档。在这种情况下,口袋妖怪的错误处理很丑陋,但是我不知道有什么好的选择。
2014年

@supercat:我只是在讲语言。但是,我认为无效的文件内容听起来不应该引发多种异常。
Magus 2014年

1
@Magus:它会引发各种异常,这就是问题所在。通常很难预料由于文件中的无效数据而可能引发的各种异常。如果不使用PokeMon处理,就有可能导致应用程序崩溃,因为例如正在加载的文件在一个需要十进制格式的32位整数的地方包含一个非常长的数字字符串,并且在其中发生了数字溢出。解析逻辑。
超级猫2014年

1

捕获一般异常是不好的,因为它会使程序处于未定义状态。您不知道哪里出了问题,所以您不知道您的程序实际上已经完成或尚未完成。

关闭程序时,我将允许捕获所有内容。只要可以清理就可以了。没有什么比您关闭的程序更令人讨厌的了,它只会抛出一个错误对话框,该对话框只会在那儿起作用,不会消失并且不会关闭计算机。

在分布式环境中,您的日志方法可能适得其反:捕获一般异常可能意味着您的程序仍然在日志文件上保持锁定,从而阻止其他用户创建日志。


0

我了解此规则的理由。但是,在实践中,如果无论是否引发异常,我都想采取相同的措施,为什么我要专门处理每一个?

就像其他人所说的那样,很难想象(即使不是不可能的)想不管抛出的异常而想要采取的措施。程序状态被破坏,任何进一步的处理都可能导致问题的情况就是一个例子(这是背后的原理Environment.FailFast)。

此外,如果我处理特定的异常,如果我正在调用的代码发生更改以在将来引发新的异常,该怎么办?现在,我必须更改代码以处理该新异常。而如果我只是捕获了Exception,则无需更改代码。

对于爱好代码,可以很好地抓住Exception,但是对于专业级代码,引入新的异常类型应与方法签名的更改(即,视为重大更改)相同。如果您同意这种观点,那么很明显,回去更改(或验证)客户端代码是唯一正确的做法。

例如,如果Foo调用Bar,并且Foo不管Bar抛出的异常类型如何都需要停止处理,那么具体确定我要捕获的异常类型是否有任何优势?

当然,因为您不会仅捕获抛出的异常Bar。Bar的客户端甚至运行时都可能在Bar调用堆栈上的时间内抛出异常。编写良好的脚本Bar应在必要时定义自己的异常类型,以便调用者可以专门捕获其自身发出的异常。

那如果我想忽略除OutOfMemoryException之外的所有异常怎么办?

恕我直言,这是考虑异常处理的错误方法。您应该操作白名单(捕获异常类型A和B),而不是黑名单(捕获除X之外的所有异常)。


通常可以指定应对所有异常执行的操作,这些异常表明尝试执行的操作失败且没有副作用,或者对系统状态产生不利影响。例如,如果用户选择要加载的文档文件,程序会将其名称传递给“使用磁盘文件中的数据创建文档对象”方法,并且该方法由于某些应用程序未曾想到的特定原因而失败(但是满足上述条件),正确的行为应该是显示“无法加载文档”消息,而不是使应用程序崩溃。
超级猫

不幸的是,没有一种标准的方法可以通过异常来表明客户端代码想要知道的最重要的事情-它是否暗示着对系统状态的任何不良影响,超出了无法执行请求的操作所隐含的含义。因此,即使很少有应处理所有异常的情况,但在许多情况下应以相同的方式处理许多异常,并且不存在所有此类异常都可以满足的标准,而某些罕见的情况也无法满足应该以不同的方式处理。
超级猫

0

也许吧。每个规则都有例外,任何规则都应严格遵守。您的示例可能是吞没所有异常的实例之一。例如,如果您想将跟踪添加到关键的生产系统中,并且要绝对确保所做的更改不会干扰应用程序的主要任务。

但是,在决定静默忽略所有失败原因之前,应仔细考虑失败的可能原因。例如,如果发生异常的原因是:

  • 日志记录方法中的编程错误,导致它总是抛出特定异常
  • 配置中日志文件的无效路径
  • 磁盘已满

您是否不想立即收到此问题的通知,以便您可以解决它?吞下该异常意味着您永远都不知道出了什么问题。

某些问题(例如磁盘已满)也可能导致应用程序的其他部分发生故障-但是现在不会记录此故障,因此您永远不会知道!


0

我想从逻辑角度而不是技术角度解决这个问题,

现在,我必须更改代码以处理该新异常。

好吧,有人将不得不处理它。那是主意。库代码的编写者在添加新的异常类型时会很谨慎,尽管因为它可能会破坏客户端,所以您不应该经常遇到这种情况。

您的问题基本上是“如果我不在乎出了什么问题该怎么办?我真的必须经历麻烦才能找出问题所在吗?”

这是美丽的部分:不,你不。

“所以,我能换个角度看看,让任何令人讨厌的东西突然冒出来,然后解决吗?”

不,那也不行。

关键是,可能的异常的集合总是大于您期望的集合,并且对您本地的小问题感兴趣。您可以处理预期的操作,如果发生意外情况,则应将其留给更高级别的处理程序,而不要吞咽。如果您不关心意外的异常,则可以押注某个调用堆栈的人,并且在异常到达处理程序之前将其杀死是一种破坏活动。

“但是……但是……那么我不关心的其他例外之一可能导致我的任务失败!”

是。但是它们永远比本地处理的重要。就像火灾报警器或老板告诉您停止正在做的事情并接管突然出现的更紧急的任务一样。


-3

您应该在每个流程的顶层捕获一般异常,并通过尽可能报告错误并终止流程来进行处理。

您不应该捕获一般的异常并尝试继续执行。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.