Google 的 Go 语言作为设计选择没有例外,Linux 名声的 Linus 称例外为垃圾。为什么?
15 回答
异常使编写代码变得非常容易,其中抛出的异常会破坏不变量并使对象处于不一致的状态。它们基本上迫使您记住,您所做的大多数语句都可能抛出并正确处理。这样做可能很棘手且违反直觉。
考虑这样一个简单的例子:
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
,但您的代码最终会纠正这一点。例外的意思是你的任何一个陈述都可以随时打断你。您有责任在每个单独的方法中正确处理并在发生这种情况时回滚,或者对您的操作进行排序,以便抛出不会影响对象状态。如果你弄错了(而且很容易犯这种错误),那么调用者最终会看到你的中间值。
像 RAII 这样的方法,C++ 程序员喜欢提到它作为这个问题的最终解决方案,在很大程度上可以防止这种情况发生。但它们不是灵丹妙药。它将确保您立即释放资源,但不会让您不必考虑对象状态的损坏和调用者看到中间值的问题。所以,对于很多人来说,更容易说,按照编码风格,没有例外。如果您限制您编写的代码类型,则更难引入这些错误。如果你不这样做,很容易犯错误。
整本书都是关于 C++ 中的异常安全编码的。很多专家都搞错了。如果它真的那么复杂并且有很多细微差别,那么这可能是一个好兆头,表明您需要忽略该功能。:-)
Go 没有异常的原因在 Go 语言设计 FAQ 中有解释:
例外是一个类似的故事。已经提出了许多异常设计,但每一种都大大增加了语言和运行时的复杂性。就其本质而言,异常跨越函数,甚至可能跨越 goroutine。它们具有广泛的影响。人们还担心它们会对图书馆产生影响。根据定义,它们是卓越的,但使用其他支持它们的语言的经验表明它们对库和接口规范具有深远的影响。如果能找到一种设计,让它们真正出类拔萃,而又不会鼓励常见错误变成需要每个程序员进行补偿的特殊控制流,那就太好了。
像泛型一样,异常仍然是一个悬而未决的问题。
换句话说,他们还没有想出如何以他们认为令人满意的方式支持 Go 中的异常。他们并不是说例外本身就是不好的;
更新 - 2012 年 5 月
围棋设计师现在已经从围栏上爬下来了。他们的常见问题解答现在这样说:
我们认为,将异常耦合到控制结构,如 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 和桌面应用程序代码。
异常本身并不“坏”,有时处理异常的方式往往很糟糕。在处理异常时可以应用一些准则来帮助缓解其中一些问题。其中一些包括(但肯定不限于):
- 不要使用异常来控制程序流——即不要依赖“catch”语句来改变逻辑流。这不仅会隐藏逻辑周围的各种细节,还会导致性能下降。
- 当返回的“状态”更有意义时,不要从函数内抛出异常——只在异常情况下抛出异常。创建异常是一项昂贵的性能密集型操作。例如,如果您调用一个方法打开一个文件并且该文件不存在,则抛出一个“FileNotFound”异常。如果调用确定客户帐户是否存在的方法,则返回布尔值,不要返回“CustomerNotFound”异常。
- 在确定是否处理异常时,不要使用“try...catch”子句,除非您可以对异常做一些有用的事情。如果您无法处理异常,则应该让它在调用堆栈中冒泡。否则,异常可能会被处理程序“吞下”,细节将丢失(除非您重新抛出异常)。
典型的论点是,无法判断特定代码段(取决于语言)会产生哪些异常,并且它们太像goto
s,因此很难在心理上跟踪执行。
http://www.joelonsoftware.com/items/2003/10/13.html
在这个问题上绝对没有共识。我想说,从像 Linus 这样的核心 C 程序员的角度来看,异常绝对是个坏主意。但是,典型的 Java 程序员所处的情况却大不相同。
例外还不错。它们非常适合 C++ 的 RAII 模型,这是 C++ 最优雅的地方。如果您已经有一堆代码不是异常安全的,那么在这种情况下它们就很糟糕。如果您正在编写非常低级的软件,例如 linux 操作系统,那么它们就很糟糕。如果您喜欢在代码中乱扔一堆错误返回检查,那么它们就没有帮助。如果您在抛出异常(C++ 析构函数提供)时没有资源控制计划,那么它们很糟糕。
因此,异常的一个很好的用例是......
假设您在一个项目中,并且每个控制器(大约 20 个不同的主要控制器)都扩展了一个具有操作方法的超类控制器。然后每个控制器都会做一堆彼此不同的事情,在一种情况下调用对象 B、C、D,在另一种情况下调用对象 F、G、D。在许多情况下,有大量返回代码并且每个控制器处理它的方式不同,异常在这里得到了救援。我修改了所有代码,从“D”抛出了正确的异常,在超类控制器操作方法中捕获了它,现在我们所有的控制器都是一致的。以前 D 为我们想告诉最终用户但不能告诉最终用户的 MULTIPLE 不同的错误情况返回 null,而我没有
是的,我们必须担心每个级别和任何资源清理/泄漏,但总的来说,我们的控制器都没有任何资源需要清理。
感谢上帝,我们有例外,否则我会进行巨大的重构,并在应该是一个简单的编程问题的事情上浪费太多时间。
从理论上讲,他们真的很糟糕。在完美的数学世界中,您不会遇到异常情况。看看函数式语言,它们没有副作用,因此它们几乎没有异常情况的源代码。
但是,现实是另一回事。我们总是会遇到“意外”的情况。这就是为什么我们需要例外。
我认为我们可以将异常视为 ExceptionSituationObserver 的语法糖。您只会收到异常通知。而已。
对于 Go,我认为他们会引入一些可以处理“意外”情况的东西。我猜想他们会尽量让它听起来不像异常那样具有破坏性,而更像是应用程序逻辑。但这只是我的猜测。
C++ 的异常处理范式构成了 Java 和 .net 的部分基础,它引入了一些好的概念,但也有一些严重的局限性。异常处理的关键设计意图之一是允许方法确保它们满足其后置条件或抛出异常,并确保在方法退出之前需要进行的任何清理都会发生。不幸的是,C++、Java 和 .net 的异常处理范例都未能提供任何好的方法来处理意外因素阻止执行预期清理的情况。这反过来意味着,如果发生意外情况(处理异常的 C++ 方法在堆栈展开期间发生),则必须冒着让一切都戛然而止的风险,
即使异常处理通常是好的,将无法提供处理在清理其他问题后发生的问题的好方法的异常处理范例视为不可接受也是不无道理的。这并不是说不能使用异常处理范例来设计框架,即使在多次失败的情况下也可以确保合理的行为,但是目前还没有顶级语言或框架可以做到这一点。
我还没有阅读所有其他答案,所以可能已经提到了这一点,但一个批评是它们会导致程序在长链中中断,从而在调试代码时难以追踪错误。例如,如果 Foo() 调用 Bar() 再调用 Wah() 再调用 ToString() 然后意外地将错误的数据推入 ToString() 最终看起来就像 Foo() 中的错误,这是一个几乎完全不相关的函数。
对我来说,问题很简单。许多程序员不恰当地使用异常处理程序。更多的语言资源更好。能够处理异常是好的。错误使用的一个例子是一个必须是整数而不被验证的值,或者另一个可能被除且不被检查为零除的输入......异常处理可能是避免更多工作和艰苦思考的一种简单方法,程序员可能想要做一个肮脏的快捷方式并应用异常处理......如果算法处理的某些问题本身就不确定,那么“专业代码永远不会失败”的陈述可能是虚幻的。也许在未知的情况下,异常处理程序天生就很好发挥作用。良好的编程实践是一个有争议的问题。
- 未处理的异常通常很糟糕。
- 异常处理不好是不好的(当然)。
- 异常处理的“好/坏”取决于上下文/范围和适当性,而不是为了这样做。
好吧,无聊的回答就到这里。我想这真的取决于语言。如果异常可能会留下分配的资源,则应避免使用它们。在脚本语言中,它们只是抛弃或跳过应用程序流的一部分。这本身就是令人讨厌的,但是用异常来逃避近乎致命的错误是一个可以接受的想法。
对于错误信号,我通常更喜欢错误信号。一切都取决于 API、用例和严重性,或者日志记录是否足够。此外,我正在尝试重新定义行为,throw Phonebooks()
而不是。这个想法是“异常”通常是死胡同,但“电话簿”包含有关错误恢复或替代执行路线的有用信息。(尚未找到好的用例,但请继续尝试。)