何时抛出异常?

499

我为应用程序不期望出现的每个条件创建了异常。例如 UserNameNotValidExceptionPasswordNotCorrectException 等等。

然而,有人告诉我不应该为这些情况创建异常。在我的UML中,它们确实是主流程之外的异常,那么为什么不应该是异常呢?

有关创建异常的指导或最佳实践方面的任何指导吗?


77
请重新开放,这是一个非常明智和合理的问题。任何问题都涉及一定程度的观点,但在这种情况下,我怀疑这是一个“最佳实践”的问题。 - Tim Long
24
支持重新开放。像许多其他有趣的话题一样,“取决于情况”很有用,在做决策时分析权衡是非常有用的。人们将观点与事实混淆并不能否定这一点。筛选淤泥应该留给读者来进行锻炼。 - aron
12
我同意应该重新开放这个问题,因为它涉及最佳实践。顺便说一下,最佳实践总是可以帮助他人的意见。 - Ajay Sharma
6
微软表示:“不要返回错误代码。异常是报告框架中的错误的主要方式。”“……如果成员不能成功完成其设计目的,那应该被视为执行失败,并且应该抛出异常。”。https://msdn.microsoft.com/library/ms229030%28v=vs.100%29.aspx - Matsen75
8
这些可能是完全合理的例外情况,这取决于抛出异常的方法。一个名为IsCredentialsValid(用户名,密码)的方法,如果用户名或密码无效,不应抛出异常,而应返回false。但如果身份验证失败,从数据库读取数据的方法可能合法地抛出此类异常。简而言之:如果一个方法无法完成它应该完成的任务,应该抛出异常。 - JacquesB
显示剩余2条评论
33个回答

708

我的个人准则是:当当前代码块的基本假设被发现为假时,就会抛出异常。

例1:假设我有一个函数,它应该检查任意一个类,并返回true,如果该类从List<>继承。这个函数问的问题是,“这个对象是否是List<>的后代?”这个函数永远不应该抛出异常,因为它的操作没有灰色地带-每个类要么继承自List<>,要么不继承,所以答案总是“是”或“否”。

例2:假设我有另一个函数,它检查一个 List<> 并在长度超过50时返回true,在长度小于50时返回false。该函数问的问题是,“这个列表是否有超过50个项目?”但这个问题是有假设的——它假定所给的对象是一个列表。如果我将NULL交给它,那么这个假设就是错误的。在这种情况下,如果函数返回 true false ,那么它违反了自己的规则。该函数不能返回 任何东西 并声称它正确回答了问题。因此它不返回值-而是抛出异常。

这类似于“多个问题”的谬论。每个函数都会提出一个问题。如果输入使该问题成为谬论,那么就抛出异常。对于返回void的函数,这条线更难画出,但底线是:如果函数对其输入的假设被违反,它应该抛出异常而不是正常返回。

另一方面,如果你发现你的函数经常抛出异常,那么你可能需要修改它们的假设。


29
没错!只有当函数的前提条件(即对参数的假设)被违反时,才会抛出异常! - Lightman
18
在语言学中,这种情况有时被称为“预设失败”。经典的例子是Bertrand Russell提出的:“法国国王秃头吗?”不能回答是或否(“法国国王秃头”既不真实也不虚假),因为它包含了一个错误的前提,即存在一位法国国王。当处理明确描述时,预设失败经常会出现,这在编程中很常见。例如,“列表的头部”在列表为空时就存在预设失败,这时适合抛出异常。 - Mohan
这可能是最好的解释! - Gaurav
1
非常感谢你。"法国国王秃头吗?" 我在研究迈农的丛林时听过这个问题.... :) 谢谢。@Mohan - ErlichBachman
1
虽然这是数十个答案中最好的,但它仍然没有表达一个简单的规则:仅在函数无法履行其契约(希望尽力而为后)时才抛出异常。这是告诉调用者所请求的事情没有发生的接受方式。称之为“违反假设”接近,但并没有完全达到要点。这不是关于内部假设,而是关于与调用者的接口。 - Ralf Kleberhoff
显示剩余3条评论

300

因为它们是正常发生的事情。异常情况并不是控制流机制。用户经常输错密码,这不是一个异常情况。异常应该是真正罕见的事情,如UserHasDiedAtKeyboard类型的情况。


5
不。如果不需要最大的性能,异常可以用作控制流机制,这对于大多数Web应用程序是正确的。Python使用异常“StopIteration”来结束迭代器,并且它运行得非常好。与IO等相比,成本微不足道。 - Seun Osewa
13
+1 很好的回答。我对那些我必须使用并为每件小事抛出异常的API开发人员感到非常沮丧。非常少数情况确实需要异常。如果你定义了25种不同的异常,请再次检查你的设计,你可能做错了。 - 7wp
50
异常是控制流机制,你可以抛出它们,也可以捕获它们,这样就可以将控制权移交给另一段代码。这就是控制流。语言之所以有异常,仅仅是为了让你能够编写直观的代码,而不需要在每次操作后询问“那个东西刚才失败了吗?”例如,Haskell没有异常,因为单子和do-notation可以自动化错误检查。 - Jesse
3
异常不仅是控制流机制,它们为(方法)客户端提供有关异常结果的有用信息,必须意识到并处理。也就是说,如果正确使用,异常可以使API更加健壮。 - idelvall
2
异常是控制流机制,相当于非局部的goto。 - starflyer
显示剩余5条评论

80

我的小建议受到伟大的书籍《Code complete》的很大影响:

  • 使用异常通知不应被忽略的事情。
  • 如果可以在本地处理错误,则不要使用异常。
  • 确保异常与例程的其余部分处于相同的抽象级别。
  • 异常应该保留给真正的例外情况。

42

如果用户名无效或密码不正确,这不是异常情况。这些是您在正常操作流程中应该预期的事情。异常是不属于正常程序操作的罕见情况。

我不喜欢使用异常,因为你不能仅凭调用语句来判断方法是否会抛出异常。这就是为什么只有在无法以合适的方式处理情况时(比如“内存不足”或“计算机着火了”),才应该使用异常。


我不喜欢使用异常,因为仅凭调用代码无法判断方法是否会抛出异常。这也是为什么支持检查异常的语言会有检查异常机制。 - Newtopian
1
已检查异常有它们自己的问题。我仍然更愿意使用“异常情况”的异常,而不是属于正常工作流程的事情。 - EricSchaefer
2
针对您的编辑。我总是在 XML 文档的摘要部分末尾放置函数抛出的异常,以便我可以在智能感知中看到该信息。 - Matthew Vines
1
当用户尝试通过操纵网页代码进行非法操作时,例如在StackOverflow上删除他人的帖子时,是否应该引发异常? - Rajat Gupta
当用户使用Firebug或其他工具修改网页代码时,这发生在本地。如果他尝试访问管理员面板或超出其权限的任何内容,我通常会让他撞上关闭的门,然后将他踢回主页并解释情况。在我的网站上,没有例外,只有一些漂亮的箭头代码。 - My1
1
听起来你在谈论如何处理错误(内存不足,计算机着火等)和处理代码异常(缺少记录,无效输入类型等)。我认为这两者之间存在明显的区别。 - Chuck Burgess

32

一个经验法则是在无法预测的情况下使用异常。例如,数据库连接、磁盘上缺少文件等。对于可以预测的情况,即用户尝试使用错误密码登录,您应该使用返回布尔值并且知道如何优雅地处理情况的函数。您不想仅因为某人输错密码而抛出异常而突然结束执行。


7
在程序执行时出现异常时,你不需要停止程序的运行。可以抛出异常,然后由调用者捕获并处理异常,如果可能的话,记录错误并继续执行。在调用栈中不断抛出异常是“不好的形式”,应该在异常发生的地方捕获并处理它。 - Krakkos
3
为什么要扔它们呢,如果你可以直接处理呢?如果密码错误或者有其他问题,我只需让if返回false并输出错误信息。 - My1
"磁盘上缺少文件" 大多数语言框架,例如 .NET 框架,都提供了用于检查文件存在性的API。为什么不在直接访问文件之前使用它们呢!" - user1451111

26

有些人认为异常不应该使用,因为如果用户输错了,那么错误的登录在正常流程中是可以预期的。我不同意这种观点,也不理解它的逻辑。拿打开文件来比较,如果文件不存在或由于某些原因无法打开,则框架将抛出异常。按照上述逻辑,这是 Microsoft 的错误。他们应该返回错误代码。同样适用于解析、Web 请求等等。

我不认为错误登录是正常流程的一部分,它是异常情况。通常情况下,用户会正确输入密码,文件确实存在。异常情况是特殊情况,对于这些情况使用异常完全没有问题。通过将返回值传播到堆栈的各个级别来使代码复杂化是一种浪费精力的做法,并且会导致混乱的代码。做最简单的可能运行的事情。不要过早地通过使用错误代码进行优化,因为异常情况很少发生,并且除非你抛出异常,否则不会产生任何成本。


除非你在调用open之前检查文件是否存在(当然这取决于你的框架),否则会抛出异常。因此,如果在检查和尝试打开文件之间文件消失了,那么就会发生异常。 - blowdart
7
文件存在并不意味着用户被允许写入文件。检查每个可能的问题非常繁琐且容易出错,并且还会重复编写代码(DRY)。 - Bjorn Reppen
无效密码异常的一个优点是,与返回代码解决方案相比的任何缓慢都不会对输入密码的人类用户产生感知。 - paperhorse
9
通过在代码中传递返回值来向堆栈的 n 层上传递错误信息会使代码变得复杂,浪费精力且容易导致代码混乱。这对我来说是使用异常的一个很好的理由。优秀的代码通常由多个小函数组成,你不想反复地在这些小函数之间传递错误代码。 - beluchin
我认为混淆的原因可能是假设“登录”类型方法的可预测结果可能是密码不正确,实际上它可能被用来确定这一点,并且在这种情况下不希望出现异常;而在“文件打开”类型的场景中,有一个特定的期望结果-如果系统由于输入参数不正确或某些外部因素无法提供结果,那么使用异常是完全合理的。 - theMayer
在代码中通过将返回值沿着堆栈向上传递n层来增加复杂性是一种浪费精力的做法,会导致代码混乱不堪。需要注意的是,这种“手动”传播具有可预测性的巨大优势。异常将故障状态传播到堆栈中的不可预测点(最近的异常处理程序),如果处理不正确,将导致更混乱的代码。 - superbadcodemonkey

23
我认为只有在无法从当前状态中恢复时,才应该抛出异常。例如,如果您正在分配内存,而没有可用的内存可以分配。对于您提到的这些情况,您可以明显地从中恢复,并可以相应地向调用者返回错误代码。
你会看到很多建议,包括这个问题的答案,认为你应该只在“异常”情况下抛出异常。这似乎表面上是合理的,但它是错误的建议,因为它用另一个主观问题(“什么是异常”)替换了一个问题(“何时应该抛出异常”)。相反,遵循Herb Sutter的建议(适用于C++,可在Dr Dobbs文章When and How to Use Exceptions以及他与Andrei Alexandrescu合著的书中,《C ++ Coding Standards》):只有当:
  • 前提条件未满足(这通常使以下操作之一不可能)或者
  • 替代方案不能满足后置条件或者
  • 替代方案不能维护不变量。
抛出异常时才遵循这种规则。为什么这样更好呢?它不是用关于前置条件、后置条件和不变量的“几个”问题替换了这个问题吗?这样做更好,有以下几个联系紧密的原因:
  • 前置条件、后置条件和不变量是我们程序(其内部API)的设计特征,而throw的决定是一个实现细节。它迫使我们记住,我们必须单独考虑设计和实现,并且我们在实现方法时的工作是生成满足设计约束的内容。
  • 它迫使我们以前置条件、后置条件和不变量的方式思考,这些是我们方法的调用者应该做出的“唯一”假设,并且可以精确地表达,从而在程序组件之间实现松耦合。
  • 这种松散的耦合使我们可以进行实现的重构(如果有必要)。
  • 后置条件和不变量是可测试的;它生成的代码可以轻松进行单元测试,因为后置条件是我们的单元测试代码可以检查(断言)的谓词。
  • 从后置条件的角度思考自然会产生一个以成功作为后置条件的设计,这是使用异常的自然风格。程序的正常(“顺利”)执行路径是按线性方式布置的,所有错误处理代码都被移至catch子句中。

这应该是被采纳的答案。 - Abdel Shokair

18

异常是一种比较昂贵的影响,例如如果用户提供了无效的密码,通常最好返回一个失败标志或其他指示它无效的数据。

这是因为异常的处理方式,真正的坏输入和独特的关键停止项应该是异常,但是登录信息不应该是异常。


10

我认为在何时使用异常没有硬性的规定。但是有使用或不使用它们的好理由:

使用异常的原因:

  • 常见情况下的代码流更清晰
  • 可以返回复杂的错误信息作为一个对象(尽管这也可以通过传递错误“out”参数的引用来实现)
  • 语言通常提供了一些管理异常事件中整洁清理的功能(Java 中的 try/finally,C# 中的 using,C++ 中的 RAII)
  • 如果没有抛出异常,执行速度 有时 可能比检查返回代码要快
  • 在 Java 中,必须声明或捕获已检查异常(尽管这可能是反对使用它们的原因)

不使用异常的原因:

  • 有时如果错误处理很简单,使用异常会过度
  • 如果未记录或声明异常,则调用代码可能未捕获它们,这可能比如果调用代码只忽略返回代码更糟糕(应用程序退出 vs 静默失败 - 哪种更糟可能取决于具体情况)
  • 在 C++ 中,使用异常的代码必须是异常安全的(即使您不抛出或捕获它们,但间接调用抛出函数)
  • 在 C++ 中,很难知道函数何时会抛出异常,因此如果使用它们,必须对异常安全保持警惕
  • 相对于检查返回标志,抛出和捕获异常通常要显着昂贵

总的来说,我更倾向于在 Java 中使用异常,而不是在 C++ 或 C# 中,因为我认为异常(声明或不声明)基本上是函数的正式接口的一部分,因为更改您的异常保证可能会破坏调用代码。在我看来,在 Java 中使用它们的最大优点是,你知道你的调用者必须处理异常,这提高了正确行为的机会。

由于这个原因,在任何编程语言中,我总是从一个共同的类派生出所有异常层的代码或API,以便调用的代码始终可以保证捕获所有异常。此外,在编写API或库时,我认为抛出特定于实现的异常类是不好的(即,将来自较低层的异常包装起来,以便你的调用者在接口上下文中理解所接收到的异常)。
请注意,Java区分一般异常和运行时异常,后者无需声明。只有在您知道错误是程序错误的结果时,才会使用运行时异常类。

6
如果代码在循环内运行,可能会一遍又一遍地引起异常,那么抛出异常是不好的,因为对于大量N来说,它们非常缓慢。但如果性能不是问题,抛出自定义异常是没有问题的。只需确保您有一个基本异常,它们都继承自该异常,称为BaseException或类似的名称。 BaseException继承自System.Exception,但是您所有的异常都继承自BaseException。您甚至可以拥有一棵异常类型树来分组相似类型,但这可能是过度设计。因此,简短的答案是,如果不会导致显着的性能损失(除非您抛出了大量异常),那么请继续使用。

1
我非常喜欢你关于循环内异常的评论,并想自己尝试一下。我编写了一个示例程序,运行了一个 int.MaxValue 次的循环,并在其中生成“除以零”的异常。IF/ELSE 版本中,我在执行除法操作之前检查被除数是否为零,完成时间为 6082 毫秒和 15407722 个时钟周期;而 TRY/CATCH 版本中,我生成异常并捕获异常,完成时间为 28174385 毫秒和 71371326155 个时钟周期:比 IF/ELSE 版本慢了整整 4632 倍。 - user1451111

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