为什么异常处理不好?

joe*_*moe 87 error-handling exception-handling exception error-reporting

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

asv*_*kau 75

异常使得编写代码非常容易,抛出异常会破坏不变量并使对象处于不一致状态.它们基本上迫使你记住你所做的大多数声明都可能抛出,并正确处理它.这样做可能很棘手且反直觉.

考虑这样的事情作为一个简单的例子:

class Frobber
{
    int m_NumberOfFrobs;
    FrobManager m_FrobManager;

public:
    void Frob()
    {
        m_NumberOfFrobs++;

        m_FrobManager.HandleFrob(new FrobObject());
    }
};
Run Code Online (Sandbox Code Playgroud)

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

这个例子可能看起来很愚蠢(好吧,我不得不伸张自己一点来构造一个:-)),但是,如果程序员不是经常考虑异常,并确保每个状态的排列都滚动,那么它就是一个例子.只要有投掷,你就会以这种方式陷入困境.

举个例子,你可以把它想象成你想到的互斥体.在一个关键部分中,您依赖于几个语句来确保数据结构不会被破坏,并且其他线程无法看到您的中间值.如果这些陈述中的任何一个只是随机地不运行,那么你最终会陷入痛苦的世界.现在拿掉锁和并发,并考虑每个方法.如果愿意,可以将每个方法视为对象状态的排列事务.在方法调用开始时,对象应该是干净状态,最后还应该是一个干净的状态.在这两者之间,变量foo可能与之不一致bar,但您的代码最终会纠正它.什么例外意味着你的任何一个陈述都可以随时打断你.在每个单独的方法中你有责任让它正确并在发生这种情况时回滚,或者命令你的操作使得抛出不会影响对象状态.如果你弄错了(很容易犯这种错误),那么调用者最终会看到你的中间值.

像CII程序员喜欢提到的RAII这样的方法作为解决这个问题的最终解决方案,可以很好地防止这种情况发生.但它们不是一颗银弹.它将确保您在throw上释放资源,但不会让您不必考虑对象状态和调用者看到中间值的损坏.因此,对于很多人来说,通过编码风格的命令,更容易说,没有例外.如果限制您编写的代码类型,则更难引入这些错误.如果不这样做,那么犯错很容易.

已经编写了关于C++中异常安全编码的全书.很多专家都弄错了.如果它真的那么复杂并且有如此多的细微差别,那么这可能是你需要忽略该功能的一个好兆头.:-)

  • 使用try-catch-finally模式以托管语言编写的异常如果写得正确则不应该留下无效状态; 由于保证了finally块的执行,因此可以在那里取消分配对象.剩下的应该由变量超出范围和垃圾收集来处理. (37认同)
  • 有趣的答案,但它并没有反映出我的编程经验.所以我想这可能是文化特定的(可能更多是Java或C++中的问题,比如Python)或特定于域. (8认同)
  • @ddaa这个问题在Python中肯定是可能的.结果通常是难以重现的错误.也许你一直特别细致或幸运.但是,你说得对,这更像是C++中的一个问题,其中来自糟糕EH的最常见错误是内存泄漏.我试图强调泄漏并不是最严重的问题.@Robert GC将缓解内存泄漏,但我不确定托管代码是否会让您免受程序员错误的影响.特别是如果有人不注意异常安全,因为他们认为这不是他们语言中的问题,那不是一个好兆头. (6认同)
  • 肯定有@lzprgmr:例外允许你处理diff.diff的错误类型.代码中的位置.处理连接错误可能需要重新连接,但不能在深层嵌套函数的中间.你想把它冒充到连接管理器或其他东西.然后处理返回值会强制您检查每次调用时的错误,并手动将其冒泡(如果出现连接重置错误).此外,返回值在嵌套调用中叠加:func3可以返回-1,func2调用func3,在错误时返回-2,为func3返回-1等. (4认同)
  • 我正在投票,但我反驳了这一点,因为这是为什么例外被瞧不起的原因.但是在我看来,几乎任何方法或代码都可能失败.您无法通过为其引入返回值来处理每个错误条件.您将丢失有关错误的信息.认为通过检查每个语句并进行清理可以保持所有内容的良好同步会导致代码非常复杂 - 在多个语句中捕获错误并清理一两个非GC的资源会更加清晰. (3认同)
  • 我不理解“m_NumberOfFrobs 的增量不会回滚”的论点以及一般关于不变量的论点。如果我们毫无例外地使用返回值,收益是多少?仍然“m_NumberOfFrobs 不会回滚”。要回滚 m_NumberOfFrobs,您必须捕获异常或检查返回值。在我看来,这两件事对于这个目的来说同样是好是坏。并且在调用 HandleFrob 10 次的情况下,异常处理允许避免代码重复。 (3认同)
  • 个人对此感到恼火,但“大多数”到底是什么意思呢?“most”的意思是“超过一半,但不是全部”,“every”的意思是“全部”,所以“most every”的意思是“超过一半,但不是全部,但全部”,这纯粹是胡言乱语。您要查找的词是“几乎”。 (2认同)
  • 至少对于 Java 开发人员来说,任何调用都可能抛出异常,这是很自然的事情。所以这不是一个隐藏的东西,它是默认的假设。因此,在您的示例中,我们只是执行逻辑操作:在 frob 成功添加到管理器后增加 m_NumberOfFrobs 。但你也必须在 Go/Rust 中做同样的事情,因为如果你递增,然后显式返回错误,你会遇到同样的问题。 (2认同)

Ste*_*n C 49

Go语言设计常见问题中解释了Go没有异常的原因:

例外是一个类似的故事.已经提出了许多异常设计,但每种设计都增加了语言和运行时的复杂性.就其本质而言,例外跨越功能,甚至可能是goroutines; 它们具有广泛的影响.人们还担心它们会对图书馆产生什么影响.根据定义,它们与支持它们的其他语言相比具有特殊的经验,表明它们对库和接口规范有深远的影响.很高兴找到一种设计,使它们真正卓越,而不会鼓励常见错误转变为需要每个程序员进行补偿的特殊控制流程.

与泛型一样,例外仍然是一个悬而未决的问题.

换句话说,他们还没有想出如何以他们认为令人满意的方式支持Go中的例外.他们并不是说例外本身就是坏事;

更新 - 2012年5月

Go设计师现在已经爬下篱笆.他们的FAQ现在说:

我们认为将异常耦合到控制结构(如try-catch-finally惯用法)会导致代码复杂化.它还倾向于鼓励程序员标记太多普通错误,例如未能打开文件,这是特殊的.

Go采用不同的方法.对于简单的错误处理,Go的多值返回使得报告错误变得容易,而不会使返回值超载.规范错误类型与Go的其他功能相结合,使错误处理变得愉快,但与其他语言完全不同.

Go还有一些内置功能,可以在真正特殊的条件下发出信号并从中恢复.恢复机制仅作为函数状态在错误发生后被拆除的一部分执行,这足以处理灾难但不需要额外的控制结构,并且如果使用得当,可以产生干净的错误处理代码.

有关详细信息,请参阅Defer,Panic和Recover文章.

所以简短的回答是他们可以使用多值返回来做不同​​的事情.(而且他们确实有一种异常处理形式.)


...... Linux的Linus称之为异常废话.

如果你想知道为什么Linus认为异常是垃圾,最好的办法是找他关于这个主题的着作.到目前为止,我唯一跟踪的是这句引言,它嵌入了几个关于C++的电子邮件中:

"整个C++异常处理事情从根本上被破坏了.对内核来说尤其破坏了."

你会注意到他特别谈论C++异常,而不是一般的异常.(和C++异常显然有一些问题,使他们棘手的正确使用.)

我的结论是,Linus根本没有调用异常(总的来说)"废话"!

  • 当他们把信息隐藏在常见问题中时,我讨厌它.:) (28认同)
  • 请注意,Linus用"C++是一种可怕的语言"开始发送电子邮件.并且继续咆哮他多么讨厌C++和选择在其中编程的人.因此,我不认为他对C++异常的看法可以被认为是可靠的,因为他对C++有些偏见. (6认同)

Rob*_*vey 29

例外情况本身并不坏,但如果你知道它们会发生很多事情,那么它们在性能方面可能会很昂贵.

经验法则是异常应该标记异常条件,并且不应该使用它们来控制程序流.

  • @Robert:"你不应该用它们来控制程序流程",我没有这么想过,对我来说是新观点:P +1 (8认同)
  • 他们不需要花太多钱.例如,你可以实现一个"尝试",花费零执行时间,并根据它在堆栈上看到的调用者地址"抛出"查找表中的异常处理程序...我会说最大的原因不是使用异常与性能完全无关. (6认同)
  • 这也确实取决于语言。例如,如果您使用Java进行编程,就很难避免异常。 (2认同)
  • @Charles:我认为关键是异常适用于表明错误,系统配置错误或输入不合理的情况.在"正常工作流程"代码中可以避免大多数Java库异常. (2认同)

Tru*_*ill 25

我不同意"只在异常情况下抛出异常".虽然一般都是正确的,但它具有误导性.例外是错误条件(执行失败).

无论您使用哪种语言,都可以获取一份" 框架设计指南:可重用.NET库的约定,惯用法和模式"(第2版).关于异常抛出的章节是没有同行的.第一版的一些引用(我工作的第二版):

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

有关于异常的好处的页面说明(API一致性,错误处理代码的位置选择,改进的健壮性等).有一个关于性能的部分包括几种模式(Tester-Doer,Try-Parse).

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

  • 我不得不对此表示不同意见.我不反对异常,这本书是必须的,但它偏向于.NET开发和C#. (3认同)
  • 我知道这很古老,只是想评论一下.NET类型和*nix类型之间似乎存在一般风格分歧.我作为Linux开发人员使用的所有库都使用了返回代码,以及我读过的*nix样式指南(如我的公司和[Google])(https://google-styleguide.googlecode.com/svn/trunk/ cppguide.xml#例外)例如)简单地说"我们不做例外".只是觉得它很有趣. (3认同)
  • 答对了。异常不仅仅是错误条件。它们是您的功能无法完成其工作、无法满足期望的任何情况。它遇到了特殊情况。如果文件丢失,openfile() 是否应该抛出异常?这取决于承诺的内容。如果文件不存在,则允许 openfile() 创建该文件,也不例外。 (2认同)

dda*_*daa 11

从golang的角度来看,我想没有异常处理可以使编译过程简单安全.

从Linus的角度来看,我理解内核代码是关于极端情况的ALL.所以拒绝异常是有道理的.

如果将当前任务放在地板上,并且常见案例代码比错误处理更重要,那么代码中的例外是有意义的.但它们需要从编译器生成代码.

例如,它们适用于大多数高级用户代码,例如Web和桌面应用程序代码.

  • 但是高级语言的存在是为了使人类编程更容易,而不是为了取悦计算机或编译器。 (3认同)
  • 但对于内核代码来说是这样,对于长时间运行的本机服务器进程来说也是如此。 (2认同)

小智 11

它本身的例外情况并非"坏",而是有时处理异常的方式往往是坏的.在处理异常时可以应用几个指南来帮助缓解其中的一些问题.其中一些包括(但肯定不限于):

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

  • 这就是人们对异常感到困惑的地方。仅当该值是函数应该为您获取的值时,您才应该从函数返回值,而不是一些意味着“错误”的带外值。您应该如何从返回字符串的函数返回错误代码?它是如此简单,以至于对人们来说太简单了;如果您的函数无法完成其工作,请抛出。就是这样。 (3认同)
  • 返回状态是一件棘手的事情.我见过太多的代码,它有一个GetCustomer方法,成功返回Customer实体,失败时返回null.在多种情况下,调用代码从不检查结果,但立即访问了客户.这大部分时间都有用...... (2认同)
  • 但是如果GetCustomer抛出异常而不是返回null,则客户端代码仍然需要处理异常.无论是通过检查null还是通过处理异常,责任在于客户端代码 - 如果它没有正确地执行任何操作,那么迟早会有一些东西爆炸. (2认同)
  • @TrueWill 支持模板/泛型的语言现在通过返回 `Option<T>` 而不是 `null` 来解决这个问题。例如,刚刚在 Java 8 中引入,受到 Guava(和其他人)的暗示。 (2认同)

Tim*_*ter 9

典型的论点是,无法判断特定代码片段(取决于语言)会产生什么异常,并且它们太像gotos,因此难以在精神上跟踪执行情况.

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

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

  • 你是否真的想接受一个重视Duct Tape程序员并且不相信单元测试是必要的人的建议?http://www.joelonsoftware.com/items/2009/09/23.html (9认同)
  • 这是在讨论中经典错误(或作弊),其中关于主题的论点被个性引用所取代.这通常是退化讨论的标志. (3认同)
  • C 代码有某种例外,只是方式不同。您需要将对一个不平凡的函数的每个调用都包装在 ifs 中,这使得使用该语言变得令人头疼! (2认同)

小智 7

例外也不错.它们与C++的RAII模型很吻合,这是关于C++最优雅的东西.如果你已经拥有一堆并非异常安全的代码,那么它们在这种情况下就不好了.如果你正在编写真正的低级软件,比如Linux操作系统,那么它们就很糟糕.如果您喜欢通过一堆错误返回检查来乱丢代码,那么它们就无济于事了.如果在抛出异常(C++析构函数提供)时没有资源控制计划,那么它们就很糟糕.

  • 即使没有例外,RAII也很有用. (6认同)
  • 但是,没有RAII(或其他一些自动资源管理),例外情况就无济于事. (4认同)

Dea*_*ler 5

因此,异常的一个很好的用例是......

假设您在一个项目中,每个控制器(大约 20 个不同的主要控制器)都使用操作方法扩展单个超类控制器。然后,每个控制器都会执行一堆彼此不同的操作,在一种情况下调用对象 B、C、D,在另一种情况下调用对象 F、G、D。在许多情况下,异常会发挥作用,因为有大量的返回代码,并且每个控制器都以不同的方式处理它。我修改了所有代码,从“D”抛出正确的异常,在超类控制器操作方法中捕获它,现在我们所有的控制器都是一致的。以前,D 对于多个不同的错误情况返回 null,我们想告诉最终用户但不能,而且我不想将 StreamResponse 变成令人讨厌的 ErrorOrStreamResponse 对象(在我看来,将数据结构与错误混合在一起是一股难闻的味道,我看到很多代码返回一个“流”或其他类型的实体,其中嵌入了错误信息(它实际上应该是函数返回成功结构或我可以使用异常与返回代码执行的错误结构)....虽然我有时可能会考虑使用 C# 的多重响应方式,但在很多情况下,异常可以跳过很多层(我不需要清理资源的层)。

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

感谢上帝,我们有例外,否则我会进行巨大的重构,并在本应是简单编程问题的问题上浪费太多时间。