Google的Go语言没有例外作为设计选择,Linux的成名Linus称之为例外废话。为什么?
Google的Go语言没有例外作为设计选择,Linux的成名Linus称之为例外废话。为什么?
Answers:
异常使编写代码变得异常容易,在该代码中抛出异常将破坏不变性并使对象处于不一致状态。从本质上讲,它们使您记住,您所做的大多数语句都可能抛出并正确处理。这样做可能很棘手,而且违反直觉。
考虑这样的一个简单示例:
class Frobber
{
int m_NumberOfFrobs;
FrobManager m_FrobManager;
public:
void Frob()
{
m_NumberOfFrobs++;
m_FrobManager.HandleFrob(new FrobObject());
}
};
假设FrobManager
会delete
的FrobObject
,这看起来不错,对吧?也许不是。。。想象一下,如果有一个FrobManager::HandleFrob()
或operator new
抛出一个异常。在此示例中,的增量m_NumberOfFrobs
不会回滚。因此,使用此实例的任何人Frobber
都会有一个可能损坏的对象。
这个例子可能看起来很愚蠢(好吧,我不得不花点时间来构造一个:-)),但是要得出的结论是,如果程序员不是一直在思考异常,并确保状态的每个排列都可以滚动每次抛出异常时,您都会遇到这种麻烦。
例如,您可以像使用互斥锁一样来考虑它。在关键部分中,您依赖于几条语句来确保数据结构没有损坏,并且其他线程看不到中间值。如果这些语句中的任何一条只是随机地不运行,那么您将陷入痛苦的世界。现在取消锁和并发性,并考虑类似的每种方法。如果可以的话,可以将每种方法都视为对象状态的置换事务。在您的方法调用开始时,对象应为干净状态,最后还应为干净状态。在两者之间,变量foo
可能与bar
,但您的代码最终会纠正这一问题。异常的意思是您的任何一条语句都可以随时打断您。每种方法都有自己的责任来使它正确并在发生这种情况时回滚,或者对您的操作进行排序,以使抛出不会影响对象状态。如果您弄错了(并且很容易犯这种错误),那么调用者最终会看到您的中间值。
C ++程序员喜欢将RAII之类的方法称为此问题的最终解决方案,它可以为防止此类问题提供很长的路要走。但是它们不是万能的。它将确保您释放资源,但不会使您不必考虑对象状态的损坏和调用者看到中间值。因此,对于很多人来说,通过编码风格的命令,没有例外就更容易说了。如果您限制编写的代码类型,则很难引入这些错误。如果不这样做,很容易犯错误。
已经编写了有关C ++中异常安全编码的全部书籍。许多专家都弄错了。如果确实如此复杂并且有许多细微差别,那么也许这是一个好信号,您需要忽略该功能。:-)
Go语言设计常见问题解答中说明了Go没有例外的原因:
例外是类似的故事。已经提出了许多用于例外的设计,但是每种设计都为语言和运行时增加了相当大的复杂性。就其本质而言,异常跨越函数甚至可能是goroutines。它们具有广泛的影响。还担心它们会对库产生影响。从定义上讲,它们是杰出的,但是在支持它们的其他语言方面的经验表明,它们对库和接口规范具有深远的影响。找到一个允许它们真正出色的设计,而又不鼓励常见错误转变为需要每个程序员进行补偿的特殊控制流程,这将是一个很好的选择。
像泛型一样,异常仍然是一个未解决的问题。
换句话说,他们还没有找到他们认为令人满意的方式来支持Go中的异常。他们并不是说例外本身是不好的;
更新-2012年5月
Go设计师现在已经脱离了障碍。他们的常见问题说明如下:
我们认为,将异常耦合到控制结构(如try-catch-finally惯用语)会导致代码混乱。它还倾向于鼓励程序员将太多的常见错误(例如,无法打开文件)标记为例外。
Go采用了不同的方法。对于简单的错误处理,Go的多值返回使报告错误变得容易,而不会使返回值过载。规范错误类型与Go的其他功能一起使错误处理令人愉悦,但与其他语言完全不同。
Go还具有一些内置功能,可以发出信号并从真正的异常状况中恢复。恢复机制仅在发生错误后被破坏的功能状态的一部分中执行,该机制足以处理灾难,但不需要额外的控制结构,如果使用得当,可以生成干净的错误处理代码。
有关详细信息,请参见延缓,紧急情况和恢复文章。
因此,简短的答案是,他们可以使用多值回报来做不同的事情。(而且它们确实有一种异常处理形式。)
...而Linux的成名者Linus称之为例外废话。
如果您想知道Linus为什么认为例外就是废话,最好的办法就是寻找他关于该主题的著作。到目前为止,我唯一能找到的就是这个引用,它嵌入在C ++的几封电子邮件中:
“整个C ++异常处理工作从根本上被破坏了。对于内核来说尤其如此。”
您会注意到,他在谈论的是C ++异常,而不是一般的异常。(而且C ++异常显然确实存在一些问题,使它们难以正确使用。)
我的结论是,Linus根本没有将异常称为“废话”!
异常本身并不坏,但是如果您知道它们会经常发生,则它们在性能方面可能会非常昂贵。
经验法则是,异常应标记异常条件,并且不应将其用于控制程序流。
我不同意“仅在特殊情况下抛出异常”。虽然通常是正确的,但它具有误导性。错误条件(执行失败)的例外。
无论使用哪种语言,都可以获取《框架设计指南:可重用.NET库的约定,惯用语和模式》(第2版)的副本。关于异常抛出的这一章没有同行。第一版(第二版在我的作品中)的一些引述:
有几页说明了异常的好处(API一致性,错误处理代码的位置选择,改进的鲁棒性等)。关于性能的一节包含了几种模式(Tester-Doer,Try-Parse)。
异常和异常处理是不坏。像任何其他功能一样,它们可能会被滥用。
从golang的角度来看,我猜想没有异常处理将使编译过程变得简单和安全。
从Linus的角度来看,我了解内核代码只涉及极端情况。因此,拒绝例外是有意义的。
如果可以将当前任务放到地板上,并且在普通案例代码比错误处理更重要的地方,则代码中的异常才有意义。但是它们需要从编译器生成代码。
例如,它们在大多数高级的,面向用户的代码(例如Web和桌面应用程序代码)中都很好。
异常本身并不是“坏”的,这是有时处理异常的方式,这往往是不好的。处理异常时,可以使用一些准则来帮助缓解其中的一些问题。其中一些包括(但肯定不限于):
Option<T>
代替null
现在来解决此问题。例如,刚刚在Java 8中引入了Guava(及其他)的提示。
典型的论点是,无法分辨特定代码段(取决于语言)会产生什么异常,并且它们太像goto
s,从而很难从心理上跟踪执行。
http://www.joelonsoftware.com/items/2003/10/13.html
在这个问题上绝对没有共识。我想说,从像Linus这样的核心C程序员的角度来看,异常绝对不是一个好主意。但是,典型的Java程序员处在截然不同的情况下。
setjmp
/的longjmp
东西,这很糟糕。
异常还不错。它们与C ++的RAII模型非常吻合,这是C ++最优雅的东西。如果您已经有一堆不安全的异常代码,那么在这种情况下它们是不好的。如果您正在编写真正的底层软件(例如linux OS),那么它们就很糟糕。如果您喜欢用大量的错误返回检查来充实代码,那么它们将无济于事。如果在抛出异常(C ++析构函数提供的异常)时没有资源控制计划,那么它们就很糟糕。
因此,一个很好的例外用例是...。
假设您在一个项目中,并且每个控制器(大约20个不同的主要控制器)都使用操作方法扩展了单个超类控制器。然后,每个控制器执行一堆彼此不同的工作,在一种情况下调用对象B,C,D,在另一种情况下调用对象F,G,D。在很多情况下,这里有很多例外情况,其中有大量的返回码,并且每个控制器对它的处理方式都不相同。我修改了所有代码,从“ D”中抛出了适当的异常,将其捕获到超类控制器操作方法中,现在我们所有的控制器都是一致的。以前,D会针对多个不同的错误情况返回null,我们希望将这些错误情况告诉最终用户,但不能,我没有
是的,我们必须担心每个级别以及任何资源清理/泄漏,但总的来说,我们的控制器之后都没有任何资源需要清理。
谢谢上帝,我们有例外,否则我本来应该进行大量的重构,而在应该是一个简单的编程问题的东西上浪费了太多时间。
从理论上讲,它们确实很糟糕。在完美的数学世界中,您无法获得例外情况。看一下功能语言,它们没有副作用,因此它们实际上没有异常情况的来源。
但是,现实是另一个故事。我们总是会遇到“意外”的情况。这就是为什么我们需要例外。
我认为我们可以将异常视为ExceptionSituationObserver的语法糖。您只收到异常通知。而已。
我认为,借助Go,他们将推出一些可应对“意外”情况的产品。我可以猜测,他们将设法使它听起来像是异常的破坏性更小,而对应用程序逻辑的破坏性更大。但这只是我的猜测。
C ++的异常处理范例构成Java的一部分,而.net则引入了一些好的概念,但也有一些严重的局限性。异常处理的主要设计意图之一是允许方法确保它们将满足其后置条件或引发异常,并且还确保将发生在方法退出之前需要进行的任何清理。不幸的是,C ++,Java和.net的异常处理范例都无法提供任何好的方法来处理意外因素阻止执行预期清除的情况。反过来,这意味着如果发生意外情况(在堆栈展开时会发生C ++处理异常的方法),所有人都必须冒着一切停止的风险,
即使异常处理通常是好的,但将异常处理范式视为无法接受的异常处理范式并不能为处理在其他问题之后清除时发生的问题提供良好的手段,这也不是不合理的。这并不是说不能使用异常处理范例来设计框架,该范例即使在发生多故障的情况下也可以确保合理的行为,但是尚无顶级语言或框架可以做到。
我还没有阅读所有其他答案,因此可能已经提到过这一点,但是有一个批评是它们会导致程序长链中断,从而在调试代码时很难跟踪错误。例如,如果Foo()调用Bar()并调用Wah()并调用ToString(),则不小心将错误的数据推入ToString()最终看起来像是Foo()中的错误,这是一个几乎完全不相关的函数。
对我来说,这个问题很简单。许多程序员不适当地使用异常处理程序。语言资源越多越好。能够处理异常是好的。不良使用的一个示例是一个值,该值必须是未经验证的整数,或者是另一个输入可能会被除而不检查是否为零的结果...异常处理可能是避免进行更多工作和思考的简便方法,程序员可能想做一个肮脏的快捷方式并应用异常处理...如果该算法处理的某些问题因其本身的性质而不确定,则“专业代码永不失败”的说法可能是虚幻的。也许在性质上未知的情况下,异常处理程序会发挥作用。良好的编程习惯尚有争议。