提问人:joemoe 提问时间:11/15/2009 最后编辑:Trottjoemoe 更新时间:8/23/2018 访问量:44739
为什么异常处理不好?[已结束]
Why is exception handling bad? [closed]
问:
Google 的 Go 语言作为设计选择没有例外,Linux 的 Linus 称例外是废话。为什么?
答:
异常本身并不坏,但如果你知道它们会经常发生,它们在性能方面可能会很昂贵。
经验法则是,异常应标记异常情况,并且不应使用它们来控制程序流。
评论
典型的论点是,没有办法判断特定代码段会出现哪些异常(取决于语言),而且它们太像 s,因此很难在脑海中跟踪执行。goto
http://www.joelonsoftware.com/items/2003/10/13.html
在这个问题上绝对没有共识。我想说的是,从像 Linus 这样的核心 C 程序员的角度来看,异常绝对是一个坏主意。然而,一个典型的 Java 程序员所处的情况却截然不同。
评论
setjmp
longjmp
例外情况还不错。它们非常适合C++的RAII模型,这是C++最优雅的地方。如果你已经有一堆代码不是特别安全的,那么它们在上下文中是坏的。如果你正在编写非常低级的软件,比如 linux 操作系统,那么它们就很糟糕了。如果你喜欢在代码中乱扔一堆错误返回检查,那么它们就没有帮助了。如果在引发异常时没有资源控制计划(C++ 析构函数提供),那么它们就很糟糕。
评论
- 未处理的异常通常是不好的。
- 异常处理不当是坏事(当然)。
- 异常处理的“好/坏”取决于上下文/范围和适当性,而不是为了这样做。
从 golang 的角度来看,我想没有异常处理可以保持编译过程简单和安全。
从 Linus 的角度来看,我理解内核代码都是关于极端情况的。因此,拒绝例外是有道理的。
异常在代码中是有意义的,因为可以将当前任务放在地板上,并且常见的案例代码比错误处理更重要。但它们需要从编译器生成代码。
例如,它们在大多数面向用户的高级代码(如 Web 和桌面应用程序代码)中都很好。
评论
从理论上讲,它们真的很糟糕。在完美的数学世界中,你不会遇到异常情况。看看函数式语言,它们没有副作用,所以它们几乎没有非特殊情况的来源。
但是,现实是另一回事。我们总是遇到“出乎意料”的情况。这就是为什么我们需要例外。
我认为我们可以将异常视为 ExceptionSituationObserver 的语法糖。您只会收到异常通知。而已。
对于 Go,我认为他们会引入一些可以处理“意外”情况的东西。我可以猜到,他们会试图让它听起来不那么具有破坏性,作为例外,而更多地作为应用程序逻辑。但这只是我的猜测。
评论
异常使编写代码变得非常容易,其中引发的异常将破坏不变量并使对象处于不一致的状态。它们基本上迫使你记住,你所做的大多数陈述都可能抛出,并正确处理。这样做可能很棘手且违反直觉。
考虑像这样简单的例子:
class Frobber
{
int m_NumberOfFrobs;
FrobManager m_FrobManager;
public:
void Frob()
{
m_NumberOfFrobs++;
m_FrobManager.HandleFrob(new FrobObject());
}
};
假设 ,这看起来没问题,对吧?或者也许不是......想象一下,如果 OR 抛出异常。在此示例中,不会回滚 的增量。因此,任何使用此实例的人都将有一个可能损坏的对象。FrobManager
delete
FrobObject
FrobManager::HandleFrob()
operator new
m_NumberOfFrobs
Frobber
这个例子可能看起来很愚蠢(好吧,我不得不稍微伸展一下自己来构造一个:-)),但是,要点是,如果程序员没有不断地考虑异常,并确保每当有抛出时,状态的每个排列都会回滚,那么你就会遇到麻烦。
举个例子,你可以把它想象成互斥锁。在关键部分中,您依靠多个语句来确保数据结构未损坏,并且其他线程无法看到您的中间值。如果这些陈述中的任何一个只是随机不运行,你最终会陷入一个痛苦的世界。现在去掉锁和并发性,像这样考虑每个方法。如果您愿意的话,可以将每个方法视为对象状态上的排列事务。在方法调用开始时,对象应为干净状态,最后也应为干净状态。在两者之间,变量可能与 不一致,但您的代码最终会纠正这一点。例外的意思是,你的任何一个陈述都可能随时打断你。在每个单独的方法中,您都有责任使其正确并在发生时回滚,或者对操作进行排序,以便抛出不会影响对象状态。如果你弄错了(而且很容易犯这种错误),那么调用者最终会看到你的中间值。foo
bar
像 RAII 这样的方法,C++程序员喜欢提到这个问题的最终解决方案,在防止这种情况方面有很长的路要走。但它们不是灵丹妙药。它将确保你在投掷时释放资源,但不能让你免于考虑对象状态的损坏和调用方看到中间值的问题。所以,对于很多人来说,更容易说,通过编码风格的法币,没有例外。如果限制编写的代码类型,则更难引入这些错误。如果你不这样做,就很容易犯错。
整本书都是关于C++中的异常安全编码的。很多专家都搞错了。如果它真的那么复杂并且有很多细微差别,也许这是一个好兆头,你需要忽略这个功能。:-)
评论
异常本身并不“坏”,只是有时处理异常的方式往往很糟糕。在处理异常时,可以应用一些准则来帮助缓解其中一些问题。其中一些包括(但肯定不限于):
- 不要使用异常来控制程序流 - 即不要依赖“catch”语句来更改逻辑流。这不仅会隐藏逻辑周围的各种细节,还可能导致性能不佳。
- 当返回的“status”更有意义时,不要从函数中抛出异常 - 仅在特殊情况下抛出异常。创建异常是一项成本高昂且性能密集型的操作。例如,如果调用某个方法来打开文件,但该文件不存在,则引发“FileNotFound”异常。如果调用确定客户帐户是否存在的方法,则返回布尔值,不要返回“CustomerNotFound”异常。
- 在确定是否处理异常时,请勿使用“try...catch“子句,除非您可以对例外执行一些有用操作。如果您无法处理异常,则应让它冒泡到调用堆栈中。否则,异常可能会被处理程序“吞噬”,并且详细信息将丢失(除非您重新引发异常)。
评论
Option<T>
null
Go 语言设计 FAQ 中解释了 Go 没有异常的原因:
例外情况也类似。一个 例外的外观设计数量有 被提议,但每个都增加了 语言的复杂性很大 和运行时。就其本质而言, 异常跨越函数,也许 甚至 goroutines;他们有 影响范围广泛。有 也担心他们的影响 会在图书馆上。他们是, 顾名思义,特殊但 具有其他语言的经验 支持他们表明他们有深刻的 对库和接口的影响 规范。如果能 找到一个允许他们的设计 真正非凡,不鼓励 常见错误变成特殊错误 需要 程序员来补偿。
与泛型一样,例外仍然是 未解决的问题。
换句话说,他们还没有弄清楚如何以他们认为令人满意的方式支持 Go 中的异常。他们并不是说例外本身是坏的;
更新 - 2012 年 5 月
Go 设计师现在已经从围栏上爬了下来。他们的常见问题解答现在是这样说的:
我们认为,将异常耦合到控制结构中,就像 try-catch-finally 习语一样,会导致代码复杂。它还倾向于鼓励程序员将太多的普通错误(例如无法打开文件)标记为异常错误。
Go 采取了不同的方法。对于简单的错误处理,Go 的多值返回可以轻松报告错误,而不会使返回值过载。规范错误类型,再加上 Go 的其他功能,使错误处理变得愉快,但与其他语言完全不同。
Go 还具有一些内置功能,可以从真正的异常情况中发出信号并从中恢复。恢复机制仅作为函数状态的一部分在发生错误后被拆除,这足以处理灾难,但不需要额外的控制结构,如果使用得当,可以产生干净的错误处理代码。
有关详细信息,请参阅延迟、紧急和恢复一文。
因此,简短的回答是,他们可以使用多值回报以不同的方式做到这一点。(无论如何,它们确实有一种异常处理形式。
...Linux 成名的 Linus 称例外是废话。
如果你想知道为什么莱纳斯认为例外是废话,最好的办法是寻找他关于这个主题的著作。到目前为止,我唯一找到的是这句话,它嵌入在C++上的几封电子邮件中:
“整个C++异常处理事情从根本上被破坏了。对于内核来说,它尤其坏了。
你会注意到,他特别谈论的是C++异常,而不是一般的异常。(C++ 异常显然确实存在一些问题,使它们难以正确使用。
我的结论是,Linus 根本没有将例外(一般来说)称为“废话”!
我不同意“只在特殊情况下抛出例外”。虽然通常是正确的,但它具有误导性。例外情况是针对错误情况(执行失败)。
无论您使用哪种语言,都可以获取一份 Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries(第 2 版)。关于异常抛出的章节是没有对等的。第一版(第二版在我的工作中)的一些引述:
- 不要返回错误代码。
- 错误代码很容易被忽略,而且经常被忽略。
- 异常是报告框架中错误的主要方式。
- 一个好的经验法则是,如果一个方法没有按照其名称所暗示的方式执行,则应将其视为方法级故障,从而导致异常。
- 如果可能,请勿对正常控制流使用异常。
有几页关于异常好处的注释(API 一致性、错误处理代码位置的选择、改进的健壮性等)。有一个关于性能的部分,包括几种模式(Tester-Doer、Try-Parse)。
异常和异常处理还不错。与任何其他功能一样,它们可能会被滥用。
评论
好吧,这里是无聊的答案。我想这真的取决于语言。如果异常可能遗漏分配的资源,则应避免使用异常。在脚本语言中,它们只是抛弃或跳过应用程序流的某些部分。这本身就很不讨人喜欢,但通过例外来逃避近乎致命的错误是一个可以接受的想法。
对于错误信号,我通常更喜欢错误信号。这一切都取决于 API、用例和严重性,或者日志记录是否足够。此外,我正在尝试重新定义行为。这个想法是“异常”通常是死胡同,但“电话簿”包含有关错误恢复或替代执行路由的有用信息。(还没有找到一个好的用例,但请继续尝试。throw Phonebooks()
C++ 的异常处理范式构成了 Java 的部分基础,反过来又是 .net,引入了一些很好的概念,但也有一些严重的局限性。异常处理的一个关键设计意图是允许方法确保它们满足其后置条件或引发异常,并确保在方法退出之前需要进行的任何清理都将发生。不幸的是,C++、Java 和 .net 的异常处理范式都无法提供任何良好的方法来处理意外因素阻止执行预期清理的情况。这反过来意味着,如果发生意外情况(在堆栈展开期间发生处理异常的C++方法),必须冒着让一切戛然而止的风险,接受这样一种可能性,即由于堆栈展开清理期间发生的问题而无法解决的情况将被误认为可以解决的情况(并且可能是, 如果清理成功),或者接受无法解决的问题(其堆栈展开清理会触发通常可以解决的异常)的可能性,可能会被忽视,因为处理后一个问题的代码声明它“已解决”。
即使异常处理通常很好,但将异常处理范式视为不可接受的,因为这种范式无法提供处理在其他问题后清理时出现的问题的良好方法,这也不是没有道理的。这并不是说不能用异常处理范式来设计一个框架,即使在多次失败的情况下也能确保合理的行为,但目前还没有一种顶级语言或框架可以做到这一点。
对我来说,这个问题很简单。许多程序员不恰当地使用异常处理程序。语言资源越多越好。能够处理异常是件好事。错误使用的一个例子是必须是整数的值,不能验证,或者另一个输入可以除法,并且不能检查除法为零......异常处理可能是一种避免更多工作和艰苦思考的简单方法,程序员可能想做一个肮脏的快捷方式并应用异常处理......“专业代码永远不会失败”这句话可能是虚幻的,如果算法处理的某些问题本身是不确定的。也许在未知的情况下,自然是好的,异常处理程序就会发挥作用。良好的编程实践是一个有争议的问题。
评论
因此,异常的一个很好的用例是......
假设您正在做一个项目,每个控制器(大约 20 个不同的主要控制器)都使用操作方法扩展单个超类控制器。然后,每个控制器都会做一堆彼此不同的事情,在一种情况下调用对象 B、C、D,在另一种情况下调用对象 F、G、D。在许多情况下,有大量返回代码并且每个控制器的处理方式都不同,因此存在例外情况。我捣毁了所有这些代码,从“D”抛出了适当的异常,在超类控制器操作方法中捕获了它,现在我们所有的控制器都是一致的。以前 D 为多个不同的错误情况返回 null,我们想告诉最终用户但不能,我不想将 StreamResponse 变成一个讨厌的 ErrorOrStreamResponse 对象(在我看来,将数据结构与错误混合在一起是一种难闻的味道,我看到很多代码返回“Stream”或其他类型的实体,其中嵌入了错误信息(它应该真的是函数返回成功结构或错误结构,我可以对异常与返回代码一起执行)....尽管我有时会考虑使用多响应的 C# 方式,但在许多情况下,异常可以跳过很多层(我不需要清理资源的层)。
是的,我们必须担心每个级别和任何资源清理/泄漏,但总的来说,我们的控制器都没有任何资源需要清理。
谢天谢地,我们有例外,否则我会进行巨大的重构,并在本应是一个简单的编程问题上浪费太多时间。
评论
我还没有阅读所有其他答案,所以这个马已经提到过了,但一个批评是它们会导致程序在长链中断,使得在调试代码时难以跟踪错误。例如,如果 Foo() 调用 Bar(),而 Bar() 调用 Wah(),后者调用 ToString(),那么不小心将错误的数据推送到 ToString() 中最终会看起来像 Foo() 中的错误,这是一个几乎完全不相关的函数。
评论