为什么异常处理不好?


89

Google的Go语言没有例外作为设计选择,Linux的成名Linus称之为例外废话。为什么?


2
ZeroMQ的创建者写了他认为用C ++编写它是错误的(主要是由于错误处理)250bpm.com/blog:4
serbaut 2012年

Go可能没有例外,但是它具有“紧急情况”,您可以从中“恢复”(虽然仍然执行延迟的语句),并且提供了非本地控制流...
Kerrek SB

这是一篇不错的文章lighterra.com/papers/exceptionsharmful(异常处理被认为有害)
masterxilo

Afaics,异常可以在Go中使用重要的样板进行模拟,尽管这一点对于从语法糖进行编译比手动编写样板更有意义。
谢尔比摩尔三世

Answers:


81

异常使编写代码变得异常容易,在该代码中抛出异常将破坏不变性并使对象处于不一致状态。从本质上讲,它们使您记住,您所做的大多数语句都可能抛出并正确处理。这样做可能很棘手,而且违反直觉。

考虑这样的一个简单示例:

class Frobber
{
    int m_NumberOfFrobs;
    FrobManager m_FrobManager;

public:
    void Frob()
    {
        m_NumberOfFrobs++;

        m_FrobManager.HandleFrob(new FrobObject());
    }
};

假设FrobManagerdeleteFrobObject,这看起来不错,对吧?也许不是。。。想象一下,如果有一个FrobManager::HandleFrob()operator new抛出一个异常。在此示例中,的增量m_NumberOfFrobs不会回滚。因此,使用此实例的任何人Frobber都会有一个可能损坏的对象。

这个例子可能看起来很愚蠢(好吧,我不得不花点时间来构造一个:-)),但是要得出的结论是,如果程序员不是一直在思考异常,并确保状态的每个排列都可以滚动每次抛出异常时,您都会遇到这种麻烦。

例如,您可以像使用互斥锁一样来考虑它。在关键部分中,您依赖于几条语句来确保数据结构没有损坏,并且其他线程看不到中间值。如果这些语句中的任何一条只是随机地不运行,那么您将陷入痛苦的世界。现在取消锁和并发性,并考虑类似的每种方法。如果可以的话,可以将每种方法都视为对象状态的置换事务。在您的方法调用开始时,对象应为干净状态,最后还应为干净状态。在两者之间,变量foo可能与bar,但您的代码最终会纠正这一问题。异常的意思是您的任何一条语句都可以随时打断您。每种方法都有自己的责任来使它正确并在发生这种情况时回滚,或者对您的操作进行排序,以使抛出不会影响对象状态。如果您弄错了(并且很容易犯这种错误),那么调用者最终会看到您的中间值。

C ++程序员喜欢将RAII之类的方法称为此问题的最终解决方案,它可以为防止此类问题提供很长的路要走。但是它们不是万能的。它将确保您释放资源,但不会使您不必考虑对象状态的损坏和调用者看到中间值。因此,对于很多人来说,通过编码风格的命令,没有例外就更容易说了。如果您限制编写的代码类型,则很难引入这些错误。如果不这样做,很容易犯错误。

已经编写了有关C ++中异常安全编码的全部书籍。许多专家都弄错了。如果确实如此复杂并且有许多细微差别,那么也许这是一个好信号,您需要忽略该功能。:-)


9
有趣的答案,但是它并没有反映我的编程经验。因此,我想这可能是特定于文化的(可能是Java或C ++中的问题,而不是Python)或特定领域。
DDAA

39
如果使用正确的方式,使用try-catch-finally模式以托管语言编写的异常永远不会离开无效状态。由于可以确保执行finally块,因此可以将对象释放在那里。其余的应该由超出范围和垃圾回收的变量来处理。
罗伯特·哈维

7
@ddaa这个问题在Python中肯定是可能的。结果通常是难以重现的错误。也许您特别细致或幸运。但是然后,您说对了,这在C ++中更多的是一个问题,其中EH不良引起的最常见错误是内存泄漏。我试图强调泄漏不是最严重的问题。@Robert GC可以减轻内存泄漏,但是我不确定托管代码是否可以使您摆脱程序员的错误。特别是,如果某人因为认为自己的语言没有问题而没有关注异常安全,那么这并不是一个好兆头。
asveikau

4
@lzprgmr当然有:异常允许您处理diff。差异中的错误类型。放置在代码中。处理连接错误可能需要重新连接,但不需要在深度嵌套的函数中间进行。您想将其冒泡给连接管理器之类的东西。然后处理返回值将迫使您检查每个调用的错误,并手动将其冒泡(例如在发生连接重置错误的情况下)。此外,返回值堆叠在嵌套调用:FUNC3可以返回-1,FUNC2调用FUNC3,返回-2对他的错误,-1 FUNC3的,等等。
abourget

3
我当时投了反对票,但我否决了这一点,因为这就是为什么人们轻视异常的原因。但是我认为,几乎任何方法或代码段都可能失败。您不能通过为每个错误条件引入返回值来处理它。您将丢失有关该错误的信息。认为可以通过检查每条语句并进行清理来使所有内容保持良好的同步,这会导致代码非常复杂-在多条语句中捕获错误并清理未经GC处理的一两个资源会更加干净。
Maarten Bodewes,2014年

50

Go语言设计常见问题解答中说明了Go没有例外的原因:

例外是类似的故事。已经提出了许多用于例外的设计,但是每种设计都为语言和运行时增加了相当大的复杂性。就其本质而言,异常跨越函数甚至可能是goroutines。它们具有广泛的影响。还担心它们会对库产生影响。从定义上讲,它们是杰出的,但是在支持它们的其他语言方面的经验表明,它们对库和接口规范具有深远的影响。找到一个允许它们真正出色的设计,而又不鼓励常见错误转变为需要每个程序员进行补偿的特殊控制流程,这将是一个很好的选择。

像泛型一样,异常仍然是一个未解决的问题。

换句话说,他们还没有找到他们认为令人满意的方式来支持Go中的异常。他们并不是说例外本身是不好的;

更新-2012年5月

Go设计师现在已经脱离了障碍。他们的常见问题说明如下:

我们认为,将异常耦合到控制结构(如try-catch-finally惯用语)会导致代码混乱。它还倾向于鼓励程序员将太多的常见错误(例如,无法打开文件)标记为例外。

Go采用了不同的方法。对于简单的错误处理,Go的多值返回使报告错误变得容易,而不会使返回值过载。规范错误类型与Go的其他功能一起使错误处理令人愉悦,但与其他语言完全不同。

Go还具有一些内置功能,可以发出信号并从真正的异常状况中恢复。恢复机制仅在发生错误后被破坏的功能状态的一部分中执行,该机制足以处理灾难,但不需要额外的控制结构,如果使用得当,可以生成干净的错误处理代码。

有关详细信息,请参见延缓,紧急情况和恢复文章。

因此,简短的答案是,他们可以使用多值回报来做不同的事情。(而且它们确实有一种异常处理形式。)


...而Linux的成名者Linus称之为例外废话。

如果您想知道Linus为什么认为例外就是废话,最好的办法就是寻找他关于该主题的著作。到目前为止,我唯一能找到的就是这个引用,它嵌入在C ++的几封电子邮件中

“整个C ++异常处理工作从根本上被破坏了。对于内核来说尤其如此。”

您会注意到,他在谈论的是C ++异常,而不是一般的异常。(而且C ++异常显然确实存在一些问题,使它们难以正确使用。)

我的结论是,Linus根本没有将异常称为“废话”!


30
当他们通过在FAQ中隐藏信息时,我会讨厌它。:)
brian d foy

7
请注意,Linus以“ C ++是一种可怕的语言”开头。并且继续抱怨他对C ++和选择使用C ++进行编程的人有多讨厌。因此,考虑到他对C ++的偏见,我认为他对C ++异常的观点不能被认为是可靠的。
法拉普

1
@ Hi-Angel-正如我引用的文字所述:“对于简单的错误处理,Go的多值返回使报告错误很容易而又不会使返回值过载。” 。就是这样。无论哪种方式,我都引用Go设计师的理论依据。如果要争论,请与>> them <<争论。
斯蒂芬·C

1
@ Hi-Angel-我没有写这些段落。我只是引用了它们。如果您对他们有疑问,则可能应该与作者联系。从理论上讲,我本可以提供有关Go语言作为上下文的教程。但坦率地说,不了解Go术语的人可以访问Go网站以了解所有含义。
斯蒂芬·C

1
“ ..导致卷积代码”很久以前,我已经用许多字符串操作编写了C程序。每种方法都是分配内存,然后检查分配是否成功。看来大多数代码只是在检查分配。难道不是令人费解吗?C ++异常对我来说是一个巨大的拯救。
robsosno

29

异常本身并不坏,但是如果您知道它们会经常发生,则它们在性能方面可能会非常昂贵。

经验法则是,异常应标记异常条件,并且不应将其用于控制​​程序流。


9
@Robert:“您不应该使用它们来控制程序流”,我没有这样考虑,对我来说是新的观点:P +1
好的,

2
这也确实取决于语言。例如,如果您使用Java进行编程,就很难避免出现异常。
Charles Salvia

2
@Charles:我认为关键是在表明错误,系统配置错误或输入不合理的情况下,例外是适当的。在“正常工作流程”代码中可以避免大多数Java库异常。
Artelius

6
他们不必花费太多。例如,您可以实现“尝试”,其执行时间为零,并让“抛出”根据在堆栈上看到的调用者地址在表中查找异常处理程序……我想说,最大的原因是使用异常与性能完全无关。
asveikau

改写;这个问题清楚地暗示了通常使用异常,或者根本不使用异常(它们是废话,甚至在语言上也不是)。您的答案仅显示了为什么将异常用于程序控制流时对性能不利。
Maarten Bodewes,2014年

26

我不同意“仅在特殊情况下抛出异常”。虽然通常是正确的,但它具有误导性。错误条件(执行失败)的例外。

无论使用哪种语言,都可以获取《框架设计指南:可重用.NET库的约定,惯用语和模式》(第2版)的副本。关于异常抛出的这一章没有同行。第一版(第二版在我的作品中)的一些引述:

  • 不要返回错误代码。
  • 错误代码通常很容易被忽略。
  • 异常是报告框架错误的主要方法。
  • 一个好的经验法则是,如果某个方法没有按照其名称的含义进行操作,则应将其视为方法级别的失败,从而导致异常。
  • 如有可能,请勿在正常的控制流程中使用例外。

有几页说明了异常的好处(API一致性,错误处理代码的位置选择,改进的鲁棒性等)。关于性能的一节包含了几种模式(Tester-Doer,Try-Parse)。

异常和异常处理是坏。像任何其他功能一样,它们可能会被滥用。


3
我对此表示不同意见。我并不反对例外,这本书是必不可少的,但是它偏向于.NET开发和C#。

3
我知道这很古老,只是想评论一下,.NET类型和* nix类型之间似乎存在一般的风格分歧。我用作Linux开发人员的所有库都使用返回码,而我阅读的* nix样式指南(例如,我公司和Google的指南)都简单地说“我们不做例外”。只是觉得很有趣。
jarvisteve 2014年

1
框架应将异常与最终用户应用程序区别对待。框架除了引发异常外,没有其他方法可以处理错误,消费者应用程序则可以。
0x6C38 '16

11

从golang的角度来看,我猜想没有异常处理将使编译过程变得简单和安全。

从Linus的角度来看,我了解内核代码只涉及极端情况。因此,拒绝例外是有意义的。

如果可以将当前任务放到地板上,并且在普通案例代码比错误处理更重要的地方,则代码中的异常才有意义。但是它们需要从编译器生成代码。

例如,它们在大多数高级的,面向用户的代码(例如Web和桌面应用程序代码)中都很好。


但是,对于内核代码正确的情况也适用于长时间运行的本机服务器进程。
Lothar 2015年

11

异常本身并不是“坏”的,这是有时处理异常的方式,这往往是不好的。处理异常时,可以使用一些准则来帮助缓解其中的一些问题。其中一些包括(但肯定不限于):

  1. 不要使用异常来控制程序流-即,不要依赖“ catch”语句来改变逻辑流。这不仅会隐藏逻辑周围的各种细节,还会导致性能下降。
  2. 当返回的“状态”更有意义时,请勿从函数内引发异常-仅在特殊情况下引发异常。创建异常是一项昂贵的性能密集型操作。例如,如果您调用一种方法来打开文件而该文件不存在,则抛出“ FileNotFound”异常。如果您调用确定客户帐户是否存在的方法,则返回布尔值,而不返回“ CustomerNotFound”异常。
  3. 在确定是否处理异常时,请勿使用“ try ... catch”子句,除非您可以对异常执行一些有用的操作。如果您无法处理该异常,则应让其冒泡到调用堆栈中。否则,异常可能会被处理程序“吞噬”,并且详细信息将丢失(除非您重新抛出异常)。

2
返回状态是一件棘手的事情。我看到太多的代码具有GetCustomer方法,如果成功则返回Customer实体,如果失败则返回null。在许多情况下,调用代码从未检查过结果,而是立即访问了客户。这在大多数时候都有效...
TrueWill

3
但是,如果GetCustomer抛出异常而不是返回null,则客户端代码仍然需要处理该异常。不管是通过检查null还是通过处理异常,责任在于客户端代码-如果它不能正确执行某些操作,则早晚会发生某些事情。
克里斯,2009年

1
@TrueWill支持模板/泛型的语言通过返回a Option<T>代替null现在来解决此问题。例如,刚刚在Java 8中引入了Guava(及其他)的提示。
Maarten Bodewes,2014年

@owlstead是的。爱也许单子。如果您的语言支持并提供模式匹配,那么这是一个很好的方法。
TrueWill 2014年

@owlstead再次,克里斯所说的仍然有很强的作用-用户必须记住调用.orElse(defaultObject)函数,或所讨论的语言所决定的任何惯用法。归根结底,最终是程序员而不是错误处理方法才是问题所在。
法拉普

9

典型的论点是,无法分辨特定代码段(取决于语言)会产生什么异常,并且它们太像gotos,从而很难从心理上跟踪执行。

http://www.joelonsoftware.com/items/2003/10/13.html

在这个问题上绝对没有共识。我想说,从像Linus这样的核心C程序员的角度来看,异常绝对不是一个好主意。但是,典型的Java程序员处在截然不同的情况下。


1
C代码具有某种例外,只是以不同的方式。您需要将对非平凡函数的每个调用包装在ifs中,这使使用该语言变得头疼!
RCIX

1
还有setjmp/的longjmp东西,这很糟糕。
Tim Sylvester,2009年

9
您是否真的想听取一位重视Duct Tape程序员但不认为必须进行单元测试的人的建议?joelonsoftware.com/items/2009/09/23.html
TrueWill

4
这是讨论中的经典错误(或作弊),其中关于该主题的争论被个性参考所代替。这通常是讨论退化的标志。
Petr Gladkikh

1
@PetrGladkikh讨论始于 OP,指的是Linus的观点……作弊被称为呼吁权威的谬误。讨论只能从那儿走上坡路,并且通过回答Linus的个性来回答为什么Linus不喜欢例外的问题不是“骗子”。
Jim Balter

7

异常还不错。它们与C ++的RAII模型非常吻合,这是C ++最优雅的东西。如果您已经有一堆不安全的异常代码,那么在这种情况下它们是不好的。如果您正在编写真正的底层软件(例如linux OS),那么它们就很糟糕。如果您喜欢用大量的错误返回检查来充实代码,那么它们将无济于事。如果在抛出异常(C ++析构函数提供的异常)时没有资源控制计划,那么它们就很糟糕。


7
即使没有例外,RAII也很有用。
Mark Ransom

4
但是,没有RAII(或其他一些自动资源管理)的情况下,异常是没有用的。
格雷格·罗杰斯

+1指出异常不合适并且异常天生就不好的情况。
法拉普

4

因此,一个很好的例外用例是...。

假设您在一个项目中,并且每个控制器(大约20个不同的主要控制器)都使用操作方法扩展了单个超类控制器。然后,每个控制器执行一堆彼此不同的工作,在一种情况下调用对象B,C,D,在另一种情况下调用对象F,G,D。在很多情况下,这里有很多例外情况,其中有大量的返回码,并且每个控制器对它的处理方式都不相同。我修改了所有代码,从“ D”中抛出了适当的异常,将其捕获到超类控制器操作方法中,现在我们所有的控制器都是一致的。以前,D会针对多个不同的错误情况返回null,我们希望将这些错误情况告诉最终用户,但不能,我没有

是的,我们必须担心每个级别以及任何资源清理/泄漏,但总的来说,我们的控制器之后都没有任何资源需要清理。

谢谢上帝,我们有例外,否则我本来应该进行大量的重构,而在应该是一个简单的编程问题的东西上浪费了太多时间。


1
+1很容易成为使用我已阅读的异常的最佳论据之一。本可以使用更详细的示例(例如,UML图,一些伪代码或一些实际代码),但是使子类始终如一地工作很重要。同样,您的证据是轶事,这表明异常在实际情况中很有用,有用性是任何语言功能的主要目的。
法拉普

另外,如果您在scala中,则可以返回Try [ResponseType],它表示异常或实际响应。然后,您可以按照我上面没有提到的相同模式进行操作,而没有实际的例外,除非将它们放入尝试中。然后,就像您拥有的每种方法都返回1 + n响应类型一样,我认为这是必需的。但是,我们在scala中返回Future [response],它的工作方式与Try类似,但是可以帮助进行更多的异步编程。
迪恩·希勒

2

从理论上讲,它们确实很糟糕。在完美的数学世界中,您无法获得例外情况。看一下功能语言,它们没有副作用,因此它们实际上没有异常情况的来源。

但是,现实是另一个故事。我们总是会遇到“意外”的情况。这就是为什么我们需要例外。

我认为我们可以将异常视为ExceptionSituationObserver的语法糖。您只收到异常通知。而已。

我认为,借助Go,他们将推出一些可应对“意外”情况的产品。我可以猜测,他们将设法使它听起来像是异常的破坏性更小,而对应用程序逻辑的破坏性更大。但这只是我的猜测。


2
“看看功能语言,它们没有副作用,因此它们实际上没有异常情况的来源。” 这是一个夸大的陈述。
斯蒂芬·C

3
数学的5/0是多少?Arcsin(200)?Sqrt(-1)?数学有很多特殊情况。
罗伯特·弗雷泽

3
这不是执行性的情况...它们只是没有意义...因此可以作为例外来实现...但是也可以作为前提条件的实现来实现,因此它取决于技术实现。
Mike Chaliy

3
@MikeChaliy-对不起,但这只是诡辩。您可以运用这种推理来说,在任何情况下,永远都不会有异常情况。实际上,没有意义(或没有确定值)的数学表达式是例外。这并不意味着需要通过抛出和捕获异常来处理它们……但是,如果您不这样做,则需要特殊值(例如Inf和Nan)或返回多个值的操作。简而言之,这些情况需要进行某种特殊处理。
Stephen C

1
计算机是状态机。不是一个完美的数学世界。
Arunav Sanyal

1

C ++的异常处理范例构成Java的一部分,而.net则引入了一些好的概念,但也有一些严重的局限性。异常处理的主要设计意图之一是允许方法确保它们将满足其后置条件或引发异常,并且还确保将发生在方法退出之前需要进行的任何清理。不幸的是,C ++,Java和.net的异常处理范例都无法提供任何好的方法来处理意外因素阻止执行预期清除的情况。反过来,这意味着如果发生意外情况(在堆栈展开时会发生C ++处理异常的方法),所有人都必须冒着一切停止的风险,

即使异常处理通常是好的,但将异常处理范式视为无法接受的异常处理范式并不能为处理在其他问题之后清除时发生的问题提供良好的手段,这也不是不合理的。这并不是说不能使用异常处理范例来设计框架,该范例即使在发生多故障的情况下也可以确保合理的行为,但是尚无顶级语言或框架可以做到。


1

我还没有阅读所有其他答案,因此可能已经提到过这一点,但是有一个批评是它们会导致程序长链中断,从而在调试代码时很难跟踪错误。例如,如果Foo()调用Bar()并调用Wah()并调用ToString(),则不小心将错误的数据推入ToString()最终看起来像是Foo()中的错误,这是一个几乎完全不相关的函数。


0
  • 不处理异常通常是不好的。
  • 错误地处理异常是不好的(当然)。
  • 异常处理的“优/劣”取决于上下文/范围和适当性,而不是出于此目的。

0

好吧,这里的答案很无聊。我想这真的取决于语言。如果异常会导致分配的资源滞后,则应避免使用它们。在脚本语言中,它们只是丢弃或跳过应用程序流的某些部分。这本身是不受欢迎的,但是可以避免发生带有例外的致命错误。

对于错误信号,我通常更喜欢错误信号。一切都取决于API,用例和严重性,或者是否满足日志记录。另外,我正在尝试重新定义行为,throw Phonebooks()而不是。“异常”通常是死胡同,但“电话簿”包含有关错误恢复或替代执行路线的有用信息。(尚未找到一个好的用例,但请继续尝试。)


0

对我来说,这个问题很简单。许多程序员不适当地使用异常处理程序。语言资源越多越好。能够处理异常是好的。不良使用的一个示例是一个值,该值必须是未经验证的整数,或者是另一个输入可能会被除而不检查是否为零的结果...异常处理可能是避免进行更多工作和思考的简便方法,程序员可能想做一个肮脏的快捷方式并应用异常处理...如果该算法处理的某些问题因其本身的性质而不确定,则“专业代码永不失败”的说法可能是虚幻的。也许在性质上未知的情况下,异常处理程序会发挥作用。良好的编程习惯尚有争议。


问题不是(也不应该是)代码是否会失败-更大的问题是,如果代码确实失败,人们在多大程度上关心细节。如果一个人试图加载一个文档而“读取数据”方法之一失败了,那么一个人通常并不在乎哪一个,因为无论如何,效果都是一样的:无法加载文档。从概念上讲,异常处理应对此有所帮助。问题在于,.NET和Java的异常处理范例没有提供任何很好的方法来区分应该混在一起的“无聊”异常和不应混在一起的“无聊”异常。
超级猫
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.