发布评论
评论(15)
Go 没有异常的原因在 Go 语言设计 FAQ 中有解释:
异常也是类似的情况。一个
例外设计的数量有
已被提议,但每个都增加了
语言的显着复杂性
和运行时。就其本质而言,
例外跨越功能,也许
甚至 goroutine;他们有
具有广泛的影响。有
也关心他们的效果
会在图书馆上。他们是,
根据定义,例外
使用其他语言的经验
支持他们表明他们有深厚的
对库和界面的影响
规格。那就太好了
找到一种设计,让他们能够
真正杰出但不鼓励
常见错误变成特殊错误
需要每个控制流
程序员来补偿。与泛型一样,异常仍然是一个
开放问题。
换句话说,他们还没有弄清楚如何以他们认为满意的方式支持 Go 中的异常。他们并不是说异常本身就是不好的;而是说异常本身是不好的。
更新 - 2012 年 5 月
Go 设计师现在已经摆脱了栅栏。他们的常见问题解答现在是这样说的:
我们认为,将异常耦合到控制结构(如 try-catch-finally 习惯用法)会导致代码变得复杂。它还倾向于鼓励程序员将太多普通错误(例如无法打开文件)标记为异常错误。
Go 采用了不同的方法。对于简单的错误处理,Go 的多值返回可以轻松报告错误,而无需重载返回值。规范的错误类型与 Go 的其他功能相结合,使错误处理变得令人愉快,但与其他语言中的错误处理有很大不同。
Go 还有一些内置函数来发出信号并从真正的异常情况中恢复。恢复机制仅作为错误后被拆除的函数状态的一部分执行,这足以处理灾难,但不需要额外的控制结构,并且如果使用得当,可以产生干净的错误处理代码。
有关详细信息,请参阅延迟、紧急和恢复文章。
所以简短的答案是他们可以使用多值回报以不同的方式做到这一点。 (无论如何,他们确实有某种形式的异常处理。)
...Linux 名声大噪的 Linus 称异常为垃圾。
如果你想知道为什么 Linus 认为异常是垃圾,最好的办法就是查找他关于该主题的著作。到目前为止,我唯一追踪到的就是嵌入在几封电子邮件中的这句话在 C++ 上:
“整个 C++ 异常处理从根本上来说已经被破坏了。对于内核来说尤其如此。”
您会注意到,他具体谈论的是 C++ 异常,而不是一般的异常。 (C++ 异常确实显然存在一些问题,使得它们很难正确使用。)
我的结论是,Linus 根本没有将异常(一般来说)称为“垃圾”!
我不同意“只在特殊情况下抛出异常”。虽然总体上是正确的,但它具有误导性。例外情况是错误情况(执行失败)。
无论您使用哪种语言,请获取框架设计指南的副本:约定、可重用 .NET 库的习语和模式(第二版)。关于异常抛出的章节是无与伦比的。第一版的一些引用(第二版是我的工作):
- 不要返回错误代码。
- 错误代码很容易被忽略,而且经常如此。
- 异常是报告框架中错误的主要方式。
- 一个好的经验法则是,如果一个方法没有按照其名称所暗示的那样执行操作,则应将其视为方法级故障,从而导致异常。
- 如果可能,请勿使用正常控制流程的异常。
有几页关于异常好处的注释(API 一致性、错误处理代码位置的选择、改进的稳健性等)。有一个关于性能的部分,其中包括多种模式(测试者-执行者、尝试-解析)。
异常和异常处理还不错。与任何其他功能一样,它们也可能被滥用。
异常本身并不是“坏”的,而是有时处理异常的方式往往是坏的。处理异常时可以应用一些准则来帮助缓解其中的一些问题。其中一些包括(但肯定不限于):
- 不要使用异常来控制程序流 - 即不要依赖“catch”语句来更改逻辑流。这不仅会隐藏逻辑周围的各种细节,还会导致性能不佳。
- 当返回的“状态”更有意义时,不要从函数内抛出异常 - 仅在异常情况下抛出异常。创建异常是一项昂贵的、性能密集型的操作。例如,如果调用打开文件的方法,但该文件不存在,则抛出“FileNotFound”异常。如果调用确定客户帐户是否存在的方法,请返回布尔值,不要返回“CustomerNotFound”异常。
- 在确定是否处理异常时,不要使用“try...catch”子句,除非您可以对异常执行一些有用的操作。如果您无法处理异常,则应该让它在调用堆栈中冒泡。否则,异常可能会被处理程序“吞噬”,并且详细信息将丢失(除非您重新抛出异常)。
典型的论点是,无法判断特定代码段(取决于语言)会出现哪些异常,而且它们太像 goto
,因此很难在心理上跟踪执行情况。
http://www.joelonsoftware.com/items/2003/10/13.html
在这个问题上肯定没有达成共识。我想说,从像 Linus 这样的核心 C 程序员的角度来看,异常绝对是一个坏主意。然而,典型的 Java 程序员的情况却截然不同。
因此,异常的一个很好的用例是……
假设您在一个项目中,每个控制器(大约 20 个不同的主要控制器)都使用操作方法扩展单个超类控制器。然后,每个控制器都会执行一系列彼此不同的操作,在一种情况下调用对象 B、C、D,在另一种情况下调用对象 F、G、D。在许多情况下,异常会发挥作用,因为有大量的返回代码,并且每个控制器都以不同的方式处理它。我修改了所有代码,从“D”抛出正确的异常,在超类控制器操作方法中捕获它,现在我们所有的控制器都是一致的。以前,D 对于多个不同的错误情况返回 null,我们想要告诉最终用户但不能,而且我不想将 StreamResponse 变成令人讨厌的 ErrorOrStreamResponse 对象(在我看来,将数据结构与错误混合在一起是一股难闻的味道,我看到很多代码返回一个“流”或其他类型的实体,其中嵌入了错误信息(它实际上应该是函数返回成功结构或我可以使用异常与返回代码执行的错误结构 但在很多情况下,异常可以跳过很多层(我不需要清理资源的层)。
)....虽然我有时可能会考虑使用 C# 方式进行响应, 我们必须担心每个级别和任何资源清理/泄漏,但总的来说,我们的控制器都没有任何资源可以清理,
感谢上帝,我们有例外,否则我会进行巨大的重构并在某些事情上浪费太多时间。这应该是一个简单的编程问题。
C++ 的异常处理范例构成了 Java 和 .net 异常处理范例的部分基础,引入了一些好的概念,但也有一些严重的局限性。异常处理的关键设计意图之一是允许方法确保它们满足后置条件或抛出异常,并确保在方法退出之前需要进行的任何清理都会发生。不幸的是,C++、Java 和 .net 的异常处理范例都无法提供任何好的方法来处理意外因素阻止执行预期清理的情况。这反过来意味着,如果发生意外情况(处理堆栈展开期间发生的异常的 C++ 方法),我们必须冒着一切都戛然而止的风险,接受由于发生问题而无法解决的情况的可能性在堆栈展开清理期间,将被误认为是可以解决的问题(如果清理成功的话,本来可以解决),或者接受这样一种可能性:无法解决的问题,其堆栈展开清理触发了通常可以解决的异常,可能会发生未被注意到,因为处理后一个问题的代码声明它“已解决”。
即使异常处理通常很好,但如果异常处理范例未能提供良好的方法来处理在清理其他问题后发生的问题,那么将其视为不可接受也并非没有道理。这并不是说框架不能设计具有异常处理范例,即使在多次失败的情况下也可以确保明智的行为,但目前还没有任何顶级语言或框架可以做到这一点。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
异常使编写代码变得非常容易,其中抛出的异常将破坏不变量并使对象处于不一致的状态。它们本质上迫使您记住,您所做的大多数语句都可能会抛出异常,并正确处理它。这样做可能很棘手并且违反直觉。
考虑这样一个简单的例子:
假设
FrobManager
将删除
FrobObject
,这看起来不错,对吧?或者也许不是……想象一下,如果FrobManager::HandleFrob()
或operator new
抛出异常。在此示例中,m_NumberOfFrobs
的增量不会回滚。因此,任何使用Frobber
实例的人都将拥有一个可能已损坏的对象。这个例子可能看起来很愚蠢(好吧,我不得不花点力气来构造一个:-)),但是,要点是,如果程序员没有不断地考虑异常,并确保状态的每个排列都得到滚动每当有投掷的时候,你都会遇到麻烦。
例如,您可以像考虑互斥体一样来考虑它。在关键部分内,您依靠多个语句来确保数据结构没有损坏并且其他线程无法看到您的中间值。如果这些语句中的任何一个没有随机运行,那么您最终会陷入一个痛苦的世界。现在去掉锁和并发,并像这样思考每个方法。如果愿意的话,可以将每个方法视为对象状态的排列事务。在方法调用开始时,对象应该处于干净状态,并且在结束时也应该处于干净状态。在这之间,变量
foo
可能与bar
不一致,但您的代码最终会纠正这一点。例外意味着你的任何一个陈述都可以随时打断你。在每个单独的方法中,您有责任确保其正确并在发生这种情况时回滚,或者对您的操作进行排序,以便抛出不会影响对象状态。如果你弄错了(而且很容易犯这种错误),那么调用者最终会看到你的中间值。像 RAII 这样的方法(C++ 程序员喜欢将其称为此问题的最终解决方案)对于防止此问题大有帮助。但它们并不是灵丹妙药。它将确保您在抛出时释放资源,但不会让您不必考虑对象状态的损坏和调用者看到中间值。因此,对于很多人来说,根据编码风格,没有例外更容易说。如果您限制编写的代码类型,则更难引入这些错误。如果不这样做,就很容易犯错误。
整本书都是关于 C++ 中的异常安全编码的。很多专家都搞错了。如果它真的那么复杂并且有那么多细微差别,也许这是一个好兆头,表明您需要忽略该功能。 :-)
Exceptions make it really easy to write code where an exception being thrown will break invariants and leave objects in an inconsistent state. They essentially force you to remember that most every statement you make can potentially throw, and handle that correctly. Doing so can be tricky and counter-intuitive.
Consider something like this as a simple example:
Assuming the
FrobManager
willdelete
theFrobObject
, this looks OK, right? Or maybe not... Imagine then if eitherFrobManager::HandleFrob()
oroperator new
throws an exception. In this example, the increment ofm_NumberOfFrobs
does not get rolled back. Thus, anyone using this instance ofFrobber
is going to have a possibly corrupted object.This example may seem stupid (ok, I had to stretch myself a bit to construct one :-)), but, the takeaway is that if a programmer isn't constantly thinking of exceptions, and making sure that every permutation of state gets rolled back whenever there are throws, you get into trouble this way.
As an example, you can think of it like you think of mutexes. Inside a critical section, you rely on several statements to make sure that data structures are not corrupted and that other threads can't see your intermediate values. If any one of those statements just randomly doesn't run, you end up in a world of pain. Now take away locks and concurrency, and think about each method like that. Think of each method as a transaction of permutations on object state, if you will. At the start of your method call, the object should be clean state, and at the end there should also be a clean state. In between, variable
foo
may be inconsistent withbar
, but your code will eventually rectify that. What exceptions mean is that any one of your statements can interrupt you at any time. The onus is on you in each individual method to get it right and roll back when that happens, or order your operations so throws don't effect object state. If you get it wrong (and it's easy to make this kind of mistake), then the caller ends up seeing your intermediate values.Methods like RAII, which C++ programmers love to mention as the ultimate solution to this problem, go a long way to protect against this. But they aren't a silver bullet. It will make sure you release resources on a throw, but doesn't free you from having to think about corruption of object state and callers seeing intermediate values. So, for a lot of people, it's easier to say, by fiat of coding style, no exceptions. If you restrict the kind of code you write, it's harder to introduce these bugs. If you don't, it's fairly easy to make a mistake.
Entire books have been written about exception safe coding in C++. Lots of experts have gotten it wrong. If it's really that complex and has so many nuances, maybe that's a good sign that you need to ignore that feature. :-)