捕获非特定exception(例如System.Exception)是不好的做法? 为什么?

我目前正在进行代码审查,以下代码让我跳了起来。 我看到这个代码存在多个问题。 你是否同意我的观点? 如果是这样,我如何向我的同事解释这是错误的(顽固型……)?

  • 捕获一般exception(Exception ex)
  • 使用“if(ex is something)”而不是另一个catch块
  • 我们吃SoapException,HttpException和WebException。 但是如果Web服务失败了,那就没什么可做的了。

码:

try { // Call to a WebService } catch (Exception ex) { if (ex is SoapException || ex is HttpException || ex is WebException) { // Log Error and eat it. } else { throw; } } 

口头禅是:

  • 如果能够正确处理exception,您应该只捕获exception

从而:

  • 你不应该抓住一般例外。

在你的情况下,是的,你应该抓住那些例外并做一些有用的事情(可能不只是吃掉它们 – 你可以在记录它们之后throw )。

你的程序员正在使用throw (而不是throw ex ),这很好

这是您可以捕获多个特定exception的方法:

 try { // Call to a WebService } catch (SoapException ex) { // Log Error and eat it } catch (HttpException ex) { // Log Error and eat it } catch (WebException ex) { // Log Error and eat it } 

这几乎与您的代码相同。 您的开发人员可能这样做是为了避免重复“日志错误并吃掉它”块。

我目前正在进行代码审查,以下代码让我跳了起来。 我看到这个代码存在多个问题。 你是否同意我的观点?

不完全,见下文。

  • 捕获一般exception(Exception ex)

一般来说,只要你重新抛出它(带有throw;),当你得出你无法处理它的结论时,捕获一般exception实际上是可以的。 代码就是这样,所以这里没有直接的问题。

  • 使用“if(ex is something)”而不是另一个catch块

catch块的净效果是实际上只处理SoapException,HttpException等,并且所有其他exception都在调用堆栈中传播。 我认为这是代码应该做的function,因此这里也没有问题。

然而,从美学和可读性POV我更喜欢多个catch块到“if(ex是SoapException || ..)”。 一旦将公共处理代码重构为方法,多个catch块只会稍微打字,对大多数开发人员来说更容易阅读。 而且,最后一击很容易被忽视。

  • 我们吃SoapException,HttpException和WebException。 但是如果Web服务失败了,那就没什么可做的了。

这里可能潜伏着代码的最大问题,但是在不了解应用程序性质的情况下很难提供建议。 如果Web服务调用正在执行您以后依赖的操作,那么仅记录和使用exception可能是错误的。 通常,您将exception传播给调用者(通常在将其包装到例如XyzWebServiceDownException之后),甚至可能在重试webservice调用几次之后(在存在虚假网络问题时更加健壮)。

捕获和重新抛出相同exception的问题在于,虽然.NET尽最大努力保持堆栈跟踪完好无损,但它总是会被修改,这可能会导致跟踪故障实际来自更加困难的地方(例如exception)行号可能看起来是重新抛出语句的行,而不是最初引发exception的行。 有关捕获/重新抛出,过滤和未捕获之间差异的更多信息。

当存在这样的重复逻辑时,您真正需要的是exceptionfilter,因此您只能捕获您感兴趣的exception类型.VB.NET具有此function,但遗憾的是C#没有。 假设的语法可能如下所示:

 try { // Call to a WebService } catch (Exception ex) if (ex is SoapException || ex is HttpException || /* etc. */) { // Log Error and eat it } 

虽然你不能这样做,但我倾向于使用lambda表达式作为公共代码(你可以在C#2.0中使用delegate ),例如

 Action logAndEat = ex => { // Log Error and eat it }; try { // Call to a WebService } catch (SoapException ex) { logAndEat(ex); } catch (HttpException ex) { logAndEat(ex); } catch (WebException ex) { logAndEat(ex); } 

有时这只是为了处理“捕获每个exception”场景,而不是实际捕获每个exception。

我认为,有时候,低级框架/运行时代码需要确保没有exception从中逃逸。 不幸的是,框架代码也无法知道线程执行的代码引发了哪些exception。

在这种情况下,可能的catch块可能如下所示:

 try { // User code called here } catch (Exception ex) { if (ExceptionIsFatal(ex)) throw; Log(ex); } 

但是,这里有三个要点:

  1. 这不适用于所有情况。 在代码审查中,我们非常挑剔那些实际上是必要的并因此允许的地方。
  2. ExceptionIsFatal()方法确保我们不会吃掉永远不会被吞下的exception(ExecutionEngineException,OutOfMemoryException,ThreadAbortException等)
  3. 仔细记录吞下的内容(event-log,log4net,YMMV)

通常,我都是为了让未被捕获的exception通过终止CLR简单地“崩溃”应用程序。 但是,特别是在服务器应用程序中,这有时是不明智的。 如果一个线程遇到一个被认为是非致命的问题,则没有理由将整个进程拆除,从而杀死所有其他正在运行的请求(例如,WCF以这种方式处理某些情况)。

我想在这里添加,因为我看到的几乎所有java / C#代码中的exception处理都是错误的。 也就是说,对于忽略的exception,你最终会遇到很难调试的错误,或者同样糟糕的是,你会得到一个晦涩的exception,它什么都不告诉你,因为盲目地追随“捕获(exception)是坏事”而事情就更糟了。

首先,要了解exception是一种促进跨代码层返回错误信息的方法。 现在,错误1:一个层不仅仅是一个堆栈帧,一个层是具有明确定义的责任的代码。 如果你只是编码接口和impl只是因为,你有更好的东西要修复。

如果图层设计得很好并且具有特定的职责,那么错误的信息在起泡时具有不同的含义。 < - 这是做什么的关键,没有普遍的规则。

因此,这意味着当出现exception时,您有2个选项,但您需要了解图层中的位置:

A)如果你处于一个层的中间,而你只是一个内部的,通常是私有的帮助函数,那就会变坏:不要担心,让调用者接收exception。 它完全可以,因为你没有业务上下文和1)你没有忽略错误和2)调用者是你的层的一部分,应该知道这可能会发生,但你现在可能不会在下面处理它。

要么 …

B)你是图层的顶部边界,是内部的立面。 然后,如果你得到一个exception,那么默认值应该是CATCH ALL并且阻止任何特定的exception进入上层,这对调用者没有意义,或者更糟糕的是,你可能会改变,调用者将依赖于一个实现细节,两者都会破裂。

应用程序的强度是层之间的解耦级别。 在这里,您将作为一般规则停止所有操作,并使用一般exception重新抛出错误,将信息转换为上层的更有意义的错误。

规则:层的所有入口点都应使用CATCH ALL保护并转换或处理所有错误。 现在这个’handeled’只发生1%的时间,大多数情况下你只需要(或者可以)以正确的抽象方式返回错误。

不,我确信这很难理解。 真实的例子 – >

我有一个包运行一些模拟。 这些模拟是在文本脚本中。 有一个包编译这些脚本,有一个通用的utils包只读取文本文件,当然还有基本的java RTL。 UML依赖是 – >

模拟器 – >编译器 – > utilsTextLoader-> Java文件

1)如果某个私有内部的utils加载器出现问题,我会得到一个FileNotFound,Permissions或者其他任何内容,只需让它通过即可。 你无能为力。

2)在边界处,在最初调用的utilsTextLoader函数中,您将遵循上述规则和CATCH_ALL。 编译器不关心发生了什么,现在只需要加载文件是否已加载。 所以在catch中,重新抛出一个新exception并将FileNotFound或其他任何内容翻译为“无法读取文件XXXX”。

3)编译器现在知道未加载源。 多数民众赞成需要知道的。 因此,如果我稍后更改utilsTestLoader以从网络加载,编译器将不会更改。 如果你放弃FileNotFound并在以后进行更改,则会影响编译器。

4)循环重复:调用文件的下层的实际函数在获取exception时不会执行任何操作。 所以它让它上升。

5)当exception到达模拟器和编译器之间的层时,编译器再次CATCHES_ALL,隐藏任何细节并抛出更具体的错误:“无法编译脚本XXX”

6)最后再重复一次循环,调用编译器的模拟器函数就让我们走了。

7)最终边界是给用户的。 用户是LAYER,全部适用。 main有一个catches_ALL的尝试,最后只是创建一个很好的对话框或页面,并向用户“抛出”翻译错误。

所以用户看到了。


模拟器:致命错误无法启动模拟器

-Compiler:无法编译脚本FOO1

–TextLoader:无法读取文件foo1.scp

— trl:FileNotFound


相比于:

a)编译器:NullPointerexception<-common case和丢失的夜晚调试文件名拼写错误

b)Loader:找不到文件< - 我是否提到加载程序加载了数百个脚本?

要么

c)没有任何事情发生,因为一切都被忽略了

当然,这假设在每次重新抛出时你都不会忘记设置原因exception。

那么我的2cts。 这个简单的规则多次挽救了我的生命……

-Ale

您通常应该仍然在全局处理程序中捕获genericsexception(这是记录意外exception的最佳位置),但如前所述,如果您打算对它们执行某些操作,则应该只捕获其他位置的特定exception类型。 catch块应该显式查找这些exception类型,而不是您发布的代码。

在这种情况下,我不认为这是一件坏事,但我也在我的代码中做了类似的事情,可以安全地忽略被捕获的exception,其余的被重新抛出。 正如Michael的回答所指出的那样,将每个catch作为一个单独的块可能会导致一些可读性问题,这些问题可以通过这条路径来防止。

关于向你的同事指出这一点,我认为你很难说服他们这是错误的做事方式 – 如果他们是顽固的话就更是如此 – 因为他们做其他事情时可能存在的可读性问题。 由于这个版本仍然抛出无法处理的通用exception,因此它符合实践的精神。 但是,如果代码符合以下条件:

 try { // Do some work } catch (Exception ex) { if (ex is SoapException) { // SoapException specific recovery actions } else if (ex is HttpException) { // HttpException specific recovery actions } else if (ex is WebException) { // WebException specific recoery actions } else { throw; } } 

然后我认为你会有更多的理由担心,因为通过在一般exception块中检查它来为特定exception做工作是没有意义的。

princeple只是为了捕捉你可以处理的exception。 例如,如果你知道如何处理findnotfound,你会捕获filenotfoundexception,其他的,不要捕获它并让它被抛到上层。