为什么异常处理不好?

时间:2009-11-15 00:38:18

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

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

15 个答案:

答案 0 :(得分:74)

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

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

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不一致,但您的代码最终会纠正它。什么例外意味着你的任何一个陈述都可以随时打断你。在每个单独的方法中你有责任让它正确并在发生这种情况时回滚,或者命令你的操作使投掷不影响对象状态。如果你弄错了(并且很容易犯这种错误),那么调用者最终会看到你的中间值。

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

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

答案 1 :(得分:49)

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

  

例外情况类似。一个   例外的设计数量   已经提出,但每个都增加   语言的复杂性   和运行时。就其本质而言,   例外跨越功能,也许   甚至是goroutines;他们有   广泛的影响。有   也关注他们的影响   会对图书馆产生影响。他们是,   根据定义,非常特殊   与其他语言的经验   支持他们表明他们有深刻的   对图书馆和界面的影响   规格。真是太好了   找到一个允许它们的设计   真正的特殊而不鼓励   常见错误变成特殊   控制流程,需要每一个   程序员要补偿。

     

与泛型一样,例外仍然存在   公开问题。

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

更新 - 2012年5月

Go设计师现在已经爬下围栏了。他们的常见问题解答现在说:

  

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

     

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

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

     

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

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


  

...... Linux的Linus称之为例外废话。

如果你想知道为什么Linus认为例外是废话,最好的办法是找他关于这个主题的着作。到目前为止我唯一跟踪的是这句引言嵌入a couple of emails on C++

  

“整个C ++异常处理事情从根本上被打破了。对于内核来说尤其突破。”

你会注意到他特别谈论C ++异常,而不是一般的例外。 (和C ++异常显然有一些问题使得它们使用起来很棘手。)

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

答案 2 :(得分:29)

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

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

答案 3 :(得分:24)

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

无论您使用何种语言,请选择Framework Design Guidelines的副本:可重用.NET库的约定,惯用法和模式(第2版)。关于异常抛出的章节是没有同行的。第一版的一些引用(我工作的第二版):

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

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

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

答案 4 :(得分:11)

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

从Linus的角度来看,我理解内核代码是关于极端情况的所有内容。因此拒绝例外是有道理的。

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

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

答案 5 :(得分:11)

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

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

答案 6 :(得分:9)

典型的论点是,没有办法告诉特定代码片段会出现什么异常(取决于语言),而且它们太像goto,因此难以在心理上跟踪执行情况。

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

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

答案 7 :(得分:7)

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

答案 8 :(得分:4)

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

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

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

谢天谢地,我们有异常,或者我会参与一个巨大的重构,浪费了太多时间在一些简单的编程问题上。

答案 9 :(得分:2)

理论上他们真的很糟糕。在完美的数学世界中,你无法获得异常情况。看一下函数式语言,它们没有副作用,所以它们几乎没有源于非常规的情况。

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

我认为我们可以将异常视为ExceptionSituationObserver的语法糖。您只需收到例外通知。没什么。

有了Go,我认为他们会介绍一些可以应对“意外”情况的东西。我可以猜测,他们会尝试让它听起来像是异常,而不是应用程序逻辑。但这只是我的猜测。

答案 10 :(得分:1)

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

即使异常处理通常是好的,将异常处理范例视为不能提供处理其他问题后清理时出现的问题的良好方法也是不合理的。这并不是说框架不能设计为具有异常处理范例,即使在多故障情况下也能确保合理的行为,但是顶级语言或框架都不能这样做。

答案 11 :(得分:1)

我还没有读过所有其他的答案,所以这个已经被提及了,但有一个批评是它们导致程序在长链中断,使得在调试代码时很难找到错误。例如,如果Foo()调用调用ToString()的Wah()调用Bar(),则意外地将错误数据推送到ToString()最终看起来像Foo()中的错误,这是一个几乎完全不相关的函数。

答案 12 :(得分:0)

  • 未处理的异常通常很糟糕。
  • 处理不当的异常是坏的(当然)。
  • 异常处理的“善/恶”取决于上下文/范围和适当性,而不是为了做到这一点。

答案 13 :(得分:0)

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

对于错误信号,我通常更喜欢错误信号。全部取决于API,用例和严重性,或者记录是否足够。此外,我正在尝试重新定义行为,而不是throw Phonebooks()。 “异常”通常是死路一条,但“电话簿”包含有关错误恢复或替代执行路径的有用信息。 (还没有找到一个好用例,但继续尝试。)

答案 14 :(得分:0)

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