【问题标题】:Why is exception handling bad? [closed]为什么异常处理不好? [关闭]
【发布时间】:2010-12-16 16:59:12
【问题描述】:

Google 的 Go 语言作为设计选择没有异常,Linux 的名气 Linus 称异常为垃圾。为什么?

【问题讨论】:

标签: exception exception-handling error-handling error-reporting


【解决方案1】:

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

经验法则是异常应该标记异常情况,并且您不应该使用它们来控制程序流。

【讨论】:

  • @Robert:“你不应该用它们来控制程序流”,我没有这样想过,对我来说是新的视角:P +1
  • 这也取决于语言。例如,如果您使用 Java 进行编程,就很难避免异常。
  • @Charles:我认为关键在于异常适用于指示错误、系统配置错误或输入不合理的情况。大多数 Java 库异常都可以在“正常工作流”代码中避免。
  • 它们不需要花费太多。例如,您可以实现一个执行时间为零的“尝试”,并让“抛出”根据它在堆栈上看到的调用者地址在表中查找异常处理程序......我会说不这样做的最大原因使用异常与性能完全无关。
  • 改写;这个问题清楚地暗示了一般使用异常,或者根本不使用异常(它们是废话,甚至不在语言中)。您的回答仅说明了为什么异常在用于程序控制流时对性能不利。
【解决方案2】:

典型的论点是无法判断特定代码段会出现哪些异常(取决于语言),并且它们太像gotos,因此很难在心理上跟踪执行。

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

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

【讨论】:

  • C 代码有某种异常,只是方式不同。您需要在 ifs 中包装对非平凡函数的每个调用,这使得使用该语言令人头疼!
  • 还有setjmp/longjmp的东西,很糟糕。
  • 你真的要听取一个重视 Duct Tape 程序员并且不相信单元测试是必要的人的建议吗? joelonsoftware.com/items/2009/09/23.html
  • 这是讨论中的典型错误(或作弊),其中有关主题的论点被人格参考所取代。这通常是讨论变质的标志。
  • @PetrGladkikh 讨论 开始 OP 指的是 Linus 的意见......这种作弊被称为诉诸权威的谬误。讨论只能从那里走上坡路,通过提及他的个性来回答Linus为什么不喜欢例外的问题并不是“作弊”。
【解决方案3】:

例外也不错。它们非常适合 C++ 的 RAII 模型,这是 C++ 最优雅的地方。如果您已经有一堆代码不是异常安全的,那么在这种情况下它们就很糟糕。如果您正在编写非常低级的软件,例如 linux 操作系统,那么它们就很糟糕。如果您喜欢在代码中乱扔一堆错误返回检查,那么它们就没有帮助。如果您在抛出异常(C++ 析构函数提供)时没有资源控制计划,那么它们很糟糕。

【讨论】:

  • RAII 即使没有例外也很有用。
  • 但是,如果没有 RAII(或其他一些自动资源管理),异常就没有用处。
  • +1 用于指出异常不合适且异常并非天生不好的情况。
【解决方案4】:
  • 未处理的异常通常很糟糕。
  • 处理不当的异常是不好的(当然)。
  • 异常处理的“好/坏”取决于上下文/范围和适当性,而不是为了这样做。

【讨论】:

    【解决方案5】:

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

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

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

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

    【讨论】:

    • 但是对于内核代码来说,对于长时间运行的本地服务器进程也是如此。
    • 但是高级语言的存在是为了让人类更容易编程,而不是为了取悦计算机或编译器。
    【解决方案6】:

    理论上他们真的很糟糕。在完美的数学世界中,您不会遇到异常情况。看看函数式语言,它们没有副作用,因此它们几乎没有异常情况的源代码。

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

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

    对于 Go,我认为他们会引入一些可以处理“意外”情况的东西。我猜想他们会尽量让它听起来不像异常那样具有破坏性,而更像是应用程序逻辑。但这只是我的猜测。

    【讨论】:

    • “看看函数式语言,它们没有副作用,因此它们几乎没有异常情况的源代码。”这是一个严重的夸大其词。
    • 数学中的 5/0 是什么?阿尔辛(200)?平方(-1)?数学有很多特殊情况。
    • 这不是特殊情况......它们只是没有意义......因此可以作为例外来实现......但也可以作为先决条件的小瓶来实现......所以它取决于关于技术实施。
    • @MikeChaliy - 对不起,这只是诡辩。您可以应用这种推理来说任何事情都没有例外情况,永远。实际上,没有意义(或没有确定值)的数学表达式是例外的。这并不意味着它们需要通过抛出和捕获异常来处理......但如果你不这样做,你需要 special 值(如 Inf 和 Nan)或返回的操作多个值。简而言之,这些情况需要某种特殊处理。
    • 计算机是状态机。不是一个完美的数学世界。
    【解决方案7】:

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

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

    class Frobber
    {
        int m_NumberOfFrobs;
        FrobManager m_FrobManager;
    
    public:
        void Frob()
        {
            m_NumberOfFrobs++;
    
            m_FrobManager.HandleFrob(new FrobObject());
        }
    };
    

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

    这个例子可能看起来很愚蠢(好吧,我不得不稍微伸展一下自己来构建一个:-)),但是,要点是,如果程序员不经常考虑异常,并确保每个排列只要有抛出,状态就会回滚,这样你就会遇到麻烦。

    例如,您可以像考虑互斥锁一样来考虑它。在临界区中,您依靠几个语句来确保数据结构没有损坏并且其他线程无法看到您的中间值。如果这些语句中的任何一个没有随机运行,那么您最终会陷入痛苦的世界。现在去掉锁和并发,然后像这样考虑每个方法。如果您愿意,可以将每种方法视为对象状态的排列事务。在方法调用开始时,对象应该是干净的状态,最后也应该是干净的状态。在这两者之间,变量foo 可能与bar 不一致,但您的代码最终会纠正这一点。例外的意思是你的任何一个陈述都可以随时打断你。您有责任在每个单独的方法中正确处理并在发生这种情况时回滚,或者对您的操作进行排序,以便抛出不会影响对象状态。如果你弄错了(而且很容易犯这种错误),那么调用者最终会看到你的中间值。

    像 RAII 这样的方法,C++ 程序员喜欢提到它作为这个问题的最终解决方案,在很大程度上可以防止这种情况发生。但它们不是灵丹妙药。它将确保您立即释放资源,但不会让您不必考虑对象状态的损坏和调用者看到中间值的问题。因此,对于很多人来说,更容易说,按照编码风格的规定,没有例外。如果您限制您编写的代码类型,则更难引入这些错误。否则,很容易出错。

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

    【讨论】:

    • 有趣的答案,但它并不能反映我的编程经验。所以我猜它要么是特定于文化的(在 Java 或 C++ 中可能比 Python 更成问题),要么是特定于域的。
    • 使用 try-catch-finally 模式以托管语言编写的异常如果编写正确,则永远不会离开无效状态;由于finally块保证被执行,对象可以在那里被释放。其余的应该由超出范围的变量和垃圾收集来处理。
    • @ddaa 这个问题在 Python 中肯定是可能的。结果通常是难以重现的错误。也许你特别细致,或者很幸运。但是,你是对的,这在 C++ 中更像是一个问题,其中来自糟糕 EH 的最常见错误是内存泄漏。我试图强调泄漏并不是最严重的问题。 @Robert GC 将减轻内存泄漏,但我不确定托管代码是否会让您摆脱程序员错误。特别是如果有人因为认为这不是他们的语言有问题而没有注意异常安全,那不是一个好兆头。
    • @lzprgmr 当然有:例外允许您处理差异。 diff 的错误类型。代码中的地方。处理连接错误可能需要重新连接,但不是在深度嵌套函数的中间。您想将其冒泡到连接管理器或其他东西。然后处理返回值会迫使您检查每次调用的错误,并手动将其冒泡(例如在连接重置错误的情况下)。此外,返回值在嵌套调用中叠加:func3 可以返回 -1,func2 调用 func3,错误返回 -2,func3 的返回值为 -1,等等。
    • 我投了反对票,但我推翻了这一点,因为这就是为什么看不起异常的原因。但是在我看来,几乎任何方法或代码都可能失败。您不能通过为其引入返回值来处理每个错误条件。您将丢失有关错误的信息。认为您可以通过检查每条语句并进行清理来保持所有内容的良好同步会导致代码非常复杂 - 在多个语句中捕获错误并清理未经过 GC 处理的一两个资源要干净得多。
    【解决方案8】:

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

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

    【讨论】:

    • 返回状态是一件棘手的事情。我见过太多的代码,其中有一个 GetCustomer 方法在成功时返回客户实体或在失败时返回 null。在多种情况下,调用代码从未检查过结果,而是立即访问了客户。这在大多数情况下都有效......
    • 但是如果GetCustomer抛出异常而不是返回null,客户端代码仍然需要处理异常。无论是通过检查 null 还是通过处理异常,责任都在于客户端代码 - 如果它没有正确地做某事,那么迟早会发生爆炸。
    • @TrueWill 支持模板/泛型的语言现在通过返回 Option<T> 而不是 null 来解决这个问题。例如,刚刚在 Java 8 中引入,从 Guava(和其他人)那里得到了提示。
    • @owlstead 是的。喜欢可能的单子。如果您的语言支持并提供模式匹配,那么这是一个不错的选择。
    • @owlstead 同样,Chris 所说的仍然适用于这一点 - 用户必须记住调用 .orElse(defaultObject) 函数,或相关语言决定的任何习语。归根结底,问题出在程序员身上,而不是错误处理方法。
    【解决方案9】:

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

    例外情况类似。一种 例外设计的数量有 已提出,但每个都增加了 语言的显着复杂性 和运行时。就其本质而言, 异常跨越函数,也许 甚至 goroutines;他们有 广泛的影响。有 也关心他们的影响 会在图书馆。他们是, 根据定义,例外但 其他语言的经验 支持他们表明他们有深刻的 对库和界面的影响 规格。会很高兴 找到一种设计,让他们成为 真正非凡而没有鼓励 常见错误变成特殊错误 需要每个控制流 程序员补偿。

    像泛型一样,异常仍然是 打开问题。

    换句话说,他们还没有弄清楚如何以他们认为令人满意的方式支持 Go 中的异常。他们并不是说例外是不好的本身

    更新 - 2012 年 5 月

    围棋设计师现在已经从围栏上爬下来了。他们的常见问题解答现在这样说:

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

    Go 采用了不同的方法。对于简单的错误处理,Go 的多值返回可以很容易地报告错误而不会重载返回值。规范的错误类型与 Go 的其他特性相结合,使错误处理变得愉快,但与其他语言中的处理完全不同。

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

    有关详细信息,请参阅延迟、恐慌和恢复文章。

    所以简短的回答是,他们可以使用多值返回以不同的方式执行此操作。 (而且它们确实有某种形式的异常处理。)


    ... Linux 的名气 Linus 称异常为垃圾。

    如果你想知道为什么 Linus 认为异常是废话,最好的办法是查找他关于该主题的著作。到目前为止,我唯一找到的就是嵌入在 a couple of emails on C++ 中的这句话:

    “整个 C++ 异常处理的东西根本就坏了。尤其是内核坏了。”

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

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

    【讨论】:

      【解决方案10】:

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

      无论您使用哪种语言,都可以获取一份Framework Design Guidelines:可重用 .NET 库的约定、惯用语和模式(第 2 版)。关于异常抛出的章节是没有同行的。第一版的一些引述(我工作的第二版):

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

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

      异常和异常处理不错不好。与任何其他功能一样,它们可能会被滥用。

      【讨论】:

      • 我不同意这一点。我不反对例外,那本书是必须的,但是它偏向于 .NET 开发和 C#。
      • 我知道这很古老,只是想评论一下,.NET 类型和 *nix 类型之间似乎存在普遍的风格分歧。我作为 Linux 开发人员使用的所有库都使用返回码,而我读过的 *nix 风格指南(例如我公司的和Google's)只是简单地说“我们不做例外”。只是觉得这很有趣。
      • 框架应该区别对待最终用户应用程序的异常。除了抛出异常之外,框架没有办法处理错误,消费者应用程序可以。
      • 宾果游戏。异常不仅仅是错误条件。它们是您的功能无法完成其工作的任何条件,无法满足期望。它遇到了特殊情况。如果文件丢失,openfile() 是否应该抛出异常?这取决于承诺的内容。如果文件不存在则允许openfile()创建文件,也不例外。
      【解决方案11】:

      好的,无聊的回答就到这里。我想这真的取决于语言。如果异常可能会留下分配的资源,则应避免使用它们。在脚本语言中,它们只是抛弃或跳过应用程序流的一部分。这本身就是不受欢迎的,但是用异常来逃避近乎致命的错误是一个可以接受的想法。

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

      【讨论】:

        【解决方案12】:

        C++ 的异常处理范式构成了 Java 的部分基础,而 .net 又引入了一些好的概念,但也有一些严重的局限性。异常处理的关键设计意图之一是允许方法确保它们满足其后置条件或抛出异常,并确保在方法退出之前需要进行的任何清理都会发生。不幸的是,C++、Java 和 .net 的异常处理范例都无法提供任何好的方法来处理意外因素阻止执行预期清理的情况。这反过来意味着,如果发生意外情况(C++ 处理异常的方法在堆栈展开期间发生),则必须冒着让一切戛然而止的风险,接受由于发生的问题而无法解决的情况的可能性在堆栈展开清理期间,清理将被误认为是可以解决的问题(如果清理成功,则可能是这样),或者接受堆栈展开清理触发通常可解决的异常的无法解决的问题可能会发生的可能性没有注意到处理后一个问题的代码声明它“已解决”。

        即使异常处理通常是好的,但如果异常处理范式未能提供处理在清理其他问题后发生的问题的好方法,那么将其视为不可接受的做法并非没有道理。这并不是说不能使用异常处理范式来设计框架,即使在多次故障情况下也能确保合理的行为,但目前还没有顶级语言或框架可以做到这一点。

        【讨论】:

          【解决方案13】:

          对我来说,问题很简单。许多程序员不恰当地使用异常处理程序。更多的语言资源更好。能够处理异常是好的。错误使用的一个例子是一个必须是整数而不被验证的值,或者另一个可能被除且不被检查为零除的输入......异常处理可能是避免更多工作和艰苦思考的一种简单方法,程序员可能想要做一个肮脏的快捷方式并应用异常处理......如果算法处理的某些问题本身就不确定,那么“专业代码永远不会失败”的陈述可能是虚幻的。也许在未知的情况下,异常处理程序天生就很好发挥作用。良好的编程实践是一个有争议的问题。

          【讨论】:

          • 问题不是(或不应该是)代码是否会失败——一个更大的问题是,如果代码确实失败了,人们关心细节的程度。如果尝试加载文档并且“读取数据”方法之一失败,则通常并不真正关心是哪一个,因为无论如何效果都是一样的:无法加载文档。从概念上讲,异常处理应该对此有好处。问题是 .NET 和 Java 的异常处理范例没有提供任何好的方法来区分应该集中在一起的“无聊”异常和不应该集中在一起的异常。
          【解决方案14】:

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

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

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

          谢天谢地,我们遇到了异常,否则我会进行大规模的重构,并在本应是一个简单的编程问题的事情上浪费太多时间。

          【讨论】:

          • +1 很容易成为我读过的使用异常的最佳论据之一。本来可以使用更详细的示例(即 UML 图、一些伪代码或一些实际代码),但是让子类始终如一地执行是一个很好的问题。此外,您的证据是轶事,这表明异常在实际情况下很有用,而有用性是任何语言功能的主要目的。
          • 作为补充,如果你在 scala 中,你可以改为返回代表异常或实际响应的 Try[ResponseType]。然后,您可以遵循我在上面忽略的相同模式,除了将它们放入 try 之外,没有实际的异常。然后就像您拥有的每个方法都返回我认为需要的 1+n 响应类型。然而,我们在 Scala 中返回 Future[response],它的工作方式与 Try 非常相似,但可以帮助进行更多的异步编程。
          【解决方案15】:

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

          【讨论】:

          • 至少在 Java 中,每个异常都会有堆栈跟踪,显示整个调用链,其中包含 ToString()。
          • 您可以将堆栈跟踪添加到 C++。使用谷歌。个人写一行如:string data = Socket(TCP).Connect("75.32.44.23").ReadAll() 如果比 SOCKET s = socket(TCP) 可读性强得多;如果(如果 s == -1)返回错误; int rval = s.connect("75.32.44.23"); if (rval == -1) 返回错误号;字符缓冲区[1024]; int rval = s.read(sizeof(buffer), buffer); if (rval == -1) 返回错误号;返回缓冲区;
          猜你喜欢
          • 2020-12-30
          • 2012-04-23
          • 1970-01-01
          • 1970-01-01
          • 2011-07-29
          • 1970-01-01
          • 2016-06-12
          • 2013-12-28
          相关资源
          最近更新 更多