为什么要保守地使用例外?

Cat*_*kul 78 c++ exception

我经常看到/听到人们说异常应该很少使用,但永远不解释原因.虽然这可能是真的,但理由通常是一种愚蠢:"它被称为例外的原因",对我来说,这似乎是一种不应被一位受人尊敬的程序员/工程师接受的解释.

可以使用异常来解决一系列问题.为什么将它们用于控制流程是不明智的?对它们的使用方式保持格外保守的理念是什么?语义?性能?复杂?美学?惯例?

我之前已经看过一些关于性能的分析,但是在与某些系统相关且与其他系统无关的水平上.

同样,我不一定不同意他们应该在特殊情况下得救,但我想知道共识的理由是什么(如果这样的事情存在的话).

小智 90

主要的摩擦点是语义.许多开发人员滥用异常并抓住每个机会.这个想法是针对某种特殊情况使用例外.例如,错误的用户输入不会被视为异常,因为您希望这种情况发生并为此做好准备.但是如果你试图创建一个文件并且磁盘上没有足够的空间,那么是的,这是一个明确的例外.

另一个问题是异常经常被抛弃和吞噬.开发人员使用这种技术简单地"沉默"程序,让它尽可能长时间运行,直到完全崩溃.这是非常错误的.如果您不处理异常,如果您没有通过释放某些资源做出适当的反应,如果您没有记录异常事件或至少没有通知用户,那么您不会使用异常来表示它们的含义.

直接回答你的问题.应该很少使用例外情况,因为特殊情况很少见,例外情况很昂贵.

很少见,因为您不希望程序在按下每个按钮或每个格式错误的用户输入时崩溃.比方说,数据库可能突然无法访问,磁盘上可能没有足够的空间,你依赖的某些第三方服务是脱机的,这一切都可能发生,但很少,这些都是明显的例外情况.

昂贵,因为抛出异常会中断正常的程序流程.运行时将展开堆栈,直到找到可以处理异常的相应异常处理程序.它还将一直收集调用信息以传递给处理程序将接收的异常对象.这一切都有成本.

这并不是说使用异常(微笑)也不例外.有时,如果您抛出异常而不是通过多层转发返回代码,它可以简化代码结构.作为一个简单的规则,如果您希望经常调用某些方法并在一半时间内发现一些"异常"情况,那么最好找到另一种解决方案.但是,如果你在大多数情况下期望正常的操作流程,而这种"特殊"情况只能在某些罕见情况下出现,那么抛出异常就可以了.

@Comments:如果可以使您的代码更简单,更容易,那么在某些不太常见的情况下绝对可以使用异常.这个选项是开放的,但我认为它在实践中非常罕见.

为什么将它们用于控制流程是不明智的?

因为异常破坏了正常的"控制流".您引发异常并放弃了程序的正常执行,可能会使对象处于不一致状态,而某些打开的资源则不同意.当然,C#有using语句,即使从使用主体抛出异常,也会确保对象被处理掉.但是让我们从语言中抽象出来.假设框架不会为您处理对象.你手动完成.您有一些系统可以请求和释放资源和内存.您在整个系统范围内都有协议,负责在什么情况下释放对象和资源.您有如何处理外部库的规则.如果程序遵循正常的操作流程,它的工作原理很好.但突然在执行过程中你抛出异常.有一半的资源未经同意.还有一半尚未被要求.如果该操作现在是交易性的,那么它就会被破坏.处理资源的规则不起作用,因为那些负责释放资源的代码部分根本不会执行.如果有其他人想要使用这些资源,他们可能会发现它们处于不一致状态并且崩溃,因为他们无法预测这种特殊情况.

比如说,你想要一个方法M()调用方法N()做一些工作并安排一些资源,然后将它返回给M(),它将使用它然后处理它.精细.现在N()出现了问题,它会抛出你在M()中没有想到的异常,所以异常会冒泡到顶部,直到它可能会被某个方法C()抓住,这个方法不知道发生了什么内心深处在N()以及是否以及如何释放一些资源.

抛出异常会创建一种方法,使您的程序进入许多新的不可预测的中间状态,这些状态难以预测,理解和处理.它有点类似于使用GOTO.设计一个可以随机将其执行从一个位置跳到另一个位置的程序是非常困难的.它也很难维护和调试.当程序的复杂性增加时,您将失去对发生的时间和地点的概述,而不是修复它.

  • @Catskul:不一定.函数返回将控件直接返回给直接调用者.抛出的异常将控制返回到整个当前调用堆栈中该异常类型(或其基类或"...")的第一个catch处理程序. (9认同)
  • 如果你比较一个抛出异常的函数和一个返回错误代码的函数,那么堆栈展开将是相同的,除非我遗漏了一些东西. (5认同)
  • 还应注意,默认情况下,调试器会在异常时断开.如果您对异常条件使用异常,调试器将会破坏很多. (5认同)
  • @jon:只有当它没有被捕获并且需要逃避更多范围以达到错误处理时才会发生这种情况.在相同的情况下,使用返回值(并且还需要向下传递多个范围)将发生相同数量的堆栈展开. (5认同)
  • -1抱歉.谈论什么是"意味着"的异常设施毫无意义.它们只是一种机制,可用于处理内存不足等异常情况. - 但这并不意味着它们*也不应该用于其他目的.我想看到的是解释为什么它们*不应该*用于其他目的.虽然你的回答确实有点讨论,但是很多关于什么例外是"意味着"的讨论. (3认同)
  • @Catskul.好吧,是的,会发生相同数量的堆栈展开,但在异常情况下,必须遍历调用堆栈以确定适当的处理程序.这是一个额外的费用,这是最初的观点.另请参阅http://www.codeproject.com/KB/cpp/exceptionhandler.aspx (2认同)
  • 实际上,Boost团队状态异常应该没有开销,实际上可以比测试错误并返回堆栈的代码更快 - http://www.boost.org/community/exception_safety.html的第2部分. (2认同)

小智 61

虽然"在特殊情况下抛出异常"是一个明智的答案,但您实际上可以定义这些情况:当满足前提条件时,但不能满足后置条件.这使您可以编写更严格,更严格和更有用的后置条件,而不会牺牲错误处理; 否则,无需例外,您必须更改后置条件以允许每个可能的错误状态.

  • 调用函数之前必须满足前提条件.
  • 后置条件是功能保证什么之后返回.
  • 异常安全性说明异常如何影响函数或数据结构的内部一致性,并且经常处理从外部传入的行为(例如,仿函数,模板参数的ctor等).

构造函数

关于每个可能用C++编写的类的构造函数,你几乎没有什么可说的,但是有一些东西.其中最主要的是构造对象(即构造函数成功返回)将被破坏. 您无法修改此后置条件,因为该语言假定它为true,并将自动调用析构函数. (从技术上讲,你可以接受未定义行为的可能性,语言保证任何东西,但这可能在其他地方更好.)

当构造函数无法成功时抛出异常的唯一替代方法是修改类的基本定义("类不变")以允许有效的"null"或僵尸状态,从而允许构造函数通过构造僵尸来"成功" .

僵尸的例子

这个僵尸修改的一个例子是std :: ifstream,你必须始终检查它的状态才能使用它.例如,因为std :: string没有,所以始终保证您可以在构造后立即使用它.想象一下,如果您必须编写此示例之类的代码,并且如果您忘记检查僵尸状态,您可能会默默地获得不正确的结果或破坏程序的其他部分:

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}
Run Code Online (Sandbox Code Playgroud)

即使命名该方法也是一个很好的例子,说明如何修改类的情境字符串的不变量和接口,既不能预测也不能处理自身.

验证输入示例

让我们来解决一个常见的例子:验证用户输入.仅仅因为我们想要允许输入失败并不意味着解析函数需要在后置条件中包含它.它确实意味着我们的处理程序需要检查解析器是否失败.

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,这显示了C++的范围界定如何要求你打破RAII/SBRM的两个例子.Python中没有这个问题并且展示了我希望C++拥有的东西的一个例子 - try-else:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n
Run Code Online (Sandbox Code Playgroud)

前提条件

不必严格检查前提条件 - 违反一个条件总是表示逻辑失败,并且它们是调用者的责任 - 但如果你检查它们,那么抛出异常是合适的.(在某些情况下,返回垃圾或使程序崩溃更合适;虽然这些操作在其他环境中可能是非常错误的.如何最好地处理未定义的行为是另一个主题.)

特别是,对比stdlib异常层次结构的std :: logic_errorstd :: runtime_error分支.前者通常用于违反前提条件,而后者更适合违反后置条件.

  • Catskul:第一个是特殊情况的定义,不是理由,也不是惯例.如果你不能保证后置条件你**不能返回**.没有例外,你必须使后置条件*非常宽*以包括所有错误状态,直到它几乎无用.看起来我没有回答你的"为什么它们应该是罕见的"字面问题,因为我不会那样看待它们.它们是一种工具,可以让我有效地收紧后置条件,同时仍然允许发生错误(..在特殊情况下:). (15认同)
  • +1.前/后条件是我听过的最好的解释. (9认同)
  • +1.这就是设计使用异常的方式,并且以这种方式使用它们实际上**提高了代码的可读性和可重用性(恕我直言). (6认同)
  • 很多人都说"但这只是归结为惯例/语义." 是啊,没错.但是约定和语义很重要,因为它们对复杂性,可用性和可维护性有深刻的影响.毕竟,决定是否正式定义前置条件和后置条件也不仅仅是约定和语义的问题?然而,它们的使用可以使您的代码更易于使用和维护. (6认同)
  • 体面的答案,但你基本上只是陈述规则,而不是提供理由.你的理性是:公约和风格? (5认同)
  • @ R.Pate,但它忽略了为什么*只*以这种方式使用它的隐含问题. (5认同)
  • MajkaRa:这很准确.构造函数没有返回值,也无法提供返回值.它成功构建对象,或者通过抛出异常而失败. (3认同)
  • @Catskul:是的!如果我可以100次赞成你的评论.R. Pate:异常投掷和处理只是一种由语言*提供的机制.当后置条件违规是不可避免的时候,那个机制*可以*用于逃避 - 但是谁或者什么说他们可能只用**那个? (3认同)
  • @R.佩特:比较:烤箱专为烹饪食物而设计.但它也可以用来加热房间.问的问题是:为什么不用烤箱加热房间?(在这种情况下,答案是:事实上你可以!但是专门设计的加热器通常会提供更快的变暖.) (3认同)
  • 类比证明是有问题的.你的不符合这种情况. (3认同)
  • MajkaRa:它构建在语言中,当构造函数返回对象时,构造了它.您可以更改类*的*定义,但不能更改构造函数保证构造对象.这与后置条件完全相同!您可以使用异常,而不是在"有效"返回值中允许错误状态.C的`atoi`是未能解释的一个很好的例子. (2认同)

Dig*_*oss 40

  1. 昂贵的
    内核调用(或其他系统API调用)来管理内核(系统)信号接口
  2. 难以分析
    goto陈述的许多问题适用于例外.它们经常在多个例程和源文件中跳过大量代码.通过阅读中间源代码,这并不总是显而易见的.(它在Java中.)
  3. 中间代码并不总能预料
    到跳过的代码可能会也可能不会被写入,并且可能会出现异常退出.如果最初是这样写的,可能没有考虑到这一点.想一想:内存泄漏,文件描述符泄漏,套接字泄漏,谁知道?
  4. 维护复杂性
    维护代码可以更加难以处理异常处理.

  • +1,一般的理由很充分.但请注意,堆栈展开和调用析构函数(至少)的成本由任何功能等效的错误处理机制支付,例如通过测试和返回错误代码,如在C. (24认同)
  • 朱利安:j_random的评论指出没有比较节省:`int f(){char*s = malloc(...); if(some_func()== error){free(s); 返回错误; 无论你是手动还是通过例外,你必须付钱来解开堆栈.你无法比较使用异常到没有错误处理**. (14认同)
  • 1.昂贵:过早优化是万恶之源.2.难以分析:与错误返回代码的嵌套层相比?我恭敬地不同意.3.中间代码并不总是预料到:与嵌套层相比,并不总是合理地处理和转换错误?新手们都失败了.4.维护并发症:怎么样?由错误代码调解的依赖关系是否更容易维护?......但这似乎是学校开发人员难以接受彼此论点的那些领域之一.与任何事情一样,错误处理设计是一种权衡. (14认同)

Eri*_*ler 22

抛出异常在某种程度上类似于goto语句.为流量控制做到这一点,你以不可理解的意大利面条代码结束.更糟糕的是,在某些情况下,您甚至不知道跳转的确切位置(即,如果您没有在给定的上下文中捕获异常).这明显违反了增强可维护性的"最少惊喜"原则.

  • 除了流量控制之外,如何将异常用于任何其他目的?即使在特殊情况下,它们仍然是一种流量控制机制,会对代码的清晰度产生影响(假设在这里:难以理解).我想你可以使用异常但禁止`catch`,虽然在这种情况下你几乎可以自己调用`std :: terminate()`.总的来说,这个论点在我看来说"永远不会使用例外",而不是"只使用很少例外". (5认同)
  • 函数内部的return语句将您带出函数.例外情况会让你知道有多少层 - 没有简单的方法可以找到. (5认同)
  • 史蒂夫:我理解你的观点,但这不是我的意思.异常情况适用于意外情况.有些人滥用它们作为"早期回报",甚至可能作为某种转换声明. (2认同)
  • 我认为这个问题假定"意外"不够充分.我同意一定程度.如果某些条件使某个函数无法按照需要完成,那么您的程序可以正确处理该情况,也可以不正确处理.如果它处理它,那么它是"预期的",并且代码需要是可理解的.如果它没有正确处理它,你就麻烦了.除非您让异常终止您的程序,否则某些级别的代码必须"期望"它.然而在实践中,正如我在答案中所说的那样,尽管理论上不令人满意,但这是一个很好的规则. (2认同)
  • 当然,包装器可以将投掷功能转换为错误返回功能,反之亦然.因此,如果您的来电者不同意您的"意外"想法,则不一定会破坏其代码可理解性.它可能会牺牲一些性能,因为它们会激发你认为"意外"的许多条件,但是它们认为是正常的和可恢复的,因此可以捕获并转换为错误或其他任何东西. (2认同)

Ste*_*sop 16

例外情况会使您更难推断您的计划状态.例如,在C++中,你必须做更多的考虑,以确保你的函数是非常安全的,比你不需要的那样.

原因是没有例外,函数调用可以返回,也可以先终止程序.除了异常,函数调用可以返回,也可以终止程序,或者它可以跳转到某个地方的catch块.因此,只需查看前面的代码,就无法再遵循控制流程.你需要知道被调用的函数是否可以抛出.您可能需要知道可以抛出什么以及它被捕获的位置,这取决于您是否关心控制在哪里,或者只关心它离开当前范围.

出于这个原因,人们说"除非情况非常特殊,否则不要使用例外".当你了解它时,"非常特殊"意味着"某些情况已经发生,其中处理错误返回值的好处被成本所抵消".所以,是的,这是一个空洞的陈述,虽然一旦你有一些"非常特殊"的直觉,它就成了一个很好的经验法则.当人们谈论流量控制时,他们意味着在本地推理的能力(不参考catch块)是返回值的好处.

Java比C++有更广泛的"非常特殊"的定义.C++程序员比Java程序员更有可能想要查看函数的返回值,因此在Java中"非常特殊"可能意味着"我无法返回非null对象作为此函数的结果".在C++中,它更可能意味着"我非常怀疑我的来电者能继续".因此,如果Java流无法读取文件,则抛出该流,而C++流(默认情况下)返回指示错误的值.但是,在所有情况下,您都希望强制调用者必须编写哪些代码.所以这确实是一个编码风格的问题:你必须达成共识,你的代码应该是什么样的,以及你要编写多少"错误检查"代码来反对你想做多少"异常安全"推理.

所有语言的广泛共识似乎是最好根据错误的可恢复性来完成(因为不可恢复的错误导致没有代码有异常,但仍需要检查并返回您自己的 - 使用错误返回的代码中的错误).所以人们开始期待"我称此功能抛出异常"意味着" 无法继续",而不仅仅是" 无法继续".这不是例外所固有的,它只是一种习惯,但就像任何好的编程实践一样,它是由聪明人提倡的习惯,他们以另一种方式尝试过而不喜欢结果.我也有过多次例外的糟糕经历.所以个人而言,我确实认为"非常特殊",除非有关情况的事情使例外特别具有吸引力.

顺便说一句,除了对代码状态的推理之外,还有性能影响.现在,例外情况通常很便宜,在您有权关心绩效的语言中.它们可能比多个级别的"哦,结果是一个错误,我最好退出自己的错误,然后".在过去的糟糕时期,人们真的担心抛出异常,抓住它并继续下一件事,会使你所做的事情变得如此缓慢以至于无用.因此,在这种情况下,"非常特殊"意味着"情况如此糟糕,以至于可怕的表现不再重要".这已不再是这种情况(尽管紧密循环中的例外情况仍然很明显)并且有希望表明为什么"非常特殊"的定义需要灵活.

  • "例如,在C++中,你必须做更多的考虑,以确保你的函数非常安全,比你不需要的那样." - 鉴于基本的C++结构(例如`new`)和标准库都抛出异常,我不知道在代码中如何不使用异常会使您无法编写异常安全的代码. (2认同)

Cha*_*via 11

确实没有达成共识.整个问题在某种程度上是主观的,因为抛出异常的"恰当性"通常是由语言本身的标准库中的现有实践所暗示的.与Java标准库相比,C++标准库抛出异常的频率要低得多,Java标准库几乎总是优先考虑异常,即使对于诸如无效用户输入之类的预期错误(例如Scanner.nextInt)也是如此.我相信,这会显着影响开发者关于什么时候抛出异常的意见.

作为一名C++程序员,我个人更喜欢为非常"特殊"的环境保留异常,例如内存不足,磁盘空间不足,启示时间等等.但我并不坚持认为这是绝对正确的做法的东西.

  • 同意 - 你经常听到"例外是针对特殊情况",但没有人愿意正确定义什么是"特殊情况" - 它主要是习惯,而且绝对是语言特定的.哎呀,在Python中,迭代器使用异常来指示序列的结束,并且它被认为是完全正常的! (4认同)
  • Catskul:几乎所有的编程都是惯例.从技术上讲,我们甚至不需要例外,或者根本不需要例外.如果它不涉及NP完全性,大O/theta/little-o或通用图灵机,它可能是惯例.:-) (3认同)
  • 我认为存在各种共识,但也许共识更多地基于惯例而不是合理的推理.可能有合理的理由只在特殊情况下使用例外,但大多数开发人员并不真正了解原因. (2认同)
  • +1.没有严格的规则,只有惯例 - 但遵守使用您的语言的其他人的惯例是有用的,因为它使程序员更容易理解彼此的代码. (2认同)

Dan*_*Tao 7

编辑11/20/2009:

我刚刚阅读了这篇关于提高托管代码性能的MSDN文章,这部分提醒了我这个问题:

抛出异常的性能成本很高.尽管结构化异常处理是处理错误条件的推荐方法,但请确保仅在出现错误情况的特殊情况下使用异常.不要将常规用于常规控制流程.

当然,这仅适用于.NET,它也专门针对那些开发高性能应用程序(如我自己); 所以这显然不是一个普遍的事实.不过,我们中有很多.NET开发人员,所以我觉得值得注意.

编辑:

好的,首先,让我们直言不讳:我无意在表演问题上与任何人争吵.事实上,事实上,我倾向于同意那些认为过早优化是罪的人.但是,我只想说两点:

  1. 海报要求传统智慧背后的客观理由,即应谨慎使用例外.我们可以讨论可读性和适当的设计; 但这些都是主观问题,人们准备在任何一方争论.我认为海报意识到了这一点.事实是,使用异常来控制程序流通常是一种低效的处理方式.不,不总是,但经常.这就是为什么谨慎使用例外是合理的建议,就像吃红肉或少喝葡萄酒的好建议一样.

  2. 优化没有充分理由和编写高效代码之间存在差异.这样做的必然结果是,写一些强大的东西(如果没有优化)和一些效率低下的东西之间存在差异.有时我认为当人们争论异常处理之类的事情时,他们实际上只是在谈论彼此,因为他们正在讨论根本不同的事情.

为了说明我的观点,请考虑以下C#代码示例.

示例1:检测无效的用户输入

这是我称之为异常滥用的一个例子.

int value = -1;
string input = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        value = int.Parse(input);
        inputChecksOut = true;

    } catch (FormatException) {
        input = GetInput();
    }
}
Run Code Online (Sandbox Code Playgroud)

对我来说,这段代码很荒谬.当然有效.没有人在争论.但它应该是这样的:

int value = -1;
string input = GetInput();

while (!int.TryParse(input, out value)) {
    input = GetInput();
}
Run Code Online (Sandbox Code Playgroud)

示例2:检查文件是否存在

我认为这种情况实际上很常见.对于很多人来说,它肯定似乎更"可接受",因为它处理文件I/O:

string text = null;
string path = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        using (FileStream fs = new FileStream(path, FileMode.Open)) {
            using (StreamReader sr = new StreamReader(fs)) {
                text = sr.ReadToEnd();
            }
        }

        inputChecksOut = true;

    } catch (FileNotFoundException) {
        path = GetInput();
    }
}
Run Code Online (Sandbox Code Playgroud)

这似乎足够合理,对吧?我们正在尝试打开一个文件; 如果不存在,我们会抓住该异常并尝试打开另一个文件......这有什么问题?

真的没什么.但请考虑这个替代方案,它不会抛出任何异常:

string text = null;
string path = GetInput();

while (!File.Exists(path)) path = GetInput();

using (FileStream fs = new FileStream(path, FileMode.Open)) {
    using (StreamReader sr = new StreamReader(fs)) {
        text = sr.ReadToEnd();
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,如果这两种方法的表现实际上是相同的,那么这实际上纯粹是一个教义问题.那么,我们来看看吧.对于第一个代码示例,我列出了10000个随机字符串,其中没有一个表示正确的整数,然后在最后添加了一个有效的整数字符串.使用上述两种方法,这些都是我的结果:

使用try/ catchblock:25.455
使用int.TryParse:1.637 毫秒

对于第二个例子,我做了基本相同的事情:制作一个10000个随机字符串的列表,其中没有一个是有效路径,然后在最后添加了一个有效路径.这些是结果:

使用try/ catchblock:29.989
使用File.Exists:22.820 毫秒

许多人会对此作出回应,他说:"是的,投掷和捕获10,000个异常是非常不现实的;这夸大了结果." 当然可以.抛出一个异常和自己处理错误输入之间的区别对于用户来说并不明显.事实仍然是,在这两种情况下,使用异常的速度比可读的替代方法慢1000到10,000倍 - 如果不是更多的话.

这就是为什么我包括GetNine()下面方法的例子.它不是无法忍受的缓慢或无法接受的缓慢; 这是因为它比它应该更慢...... 没有充分的理由.

同样,这些只是两个例子.中当然会有时候使用异常的性能损失并不严重这(帕维尔的权利;毕竟,它不依赖于实现).我所说的只是:让我们面对事实,伙计们 - 在上述情况下,投掷和捕捉异常类似于GetNine(); 这只是一种低效的做事方式,可以很容易地做得更好.


你要求的理由就好像这是每个人都不知道为什么跳上一个潮流的情况之一.但实际上答案很明显,我想你已经知道了.异常处理具有可怕的性能.

好吧,也许这对你的特殊业务场景来说很好,但相对来说,抛出/捕获异常会引入比许多情况下更多的开销.你知道,我知道:大部分时间,如果你使用异常来控制程序流,你只需编写慢速代码.

您不妨问:为什么这段代码不好?

private int GetNine() {
    for (int i = 0; i < 10; i++) {
        if (i == 9) return i;
    }
}
Run Code Online (Sandbox Code Playgroud)

我敢打赌,如果您对此功能进行了分析,您会发现它对您的典型业务应用程序执行速度非常快.这并没有改变这样一个事实,即这是一种非常低效的方式来完成可以做得更好的事情.

这就是人们谈论异常"滥用"时的意思.

  • "异常处理具有可怕的性能." - 它是一个实现细节,并不适用于所有语言,特别是对于所有实现的C++. (2认同)
  • 然而,Python很乐意使用异常来进行通用流控制,因为实现它们的性能并不高.所以,如果你的锤子看起来像一把螺丝刀,那么,责怪锤子...... (2认同)

Kon*_*zin 7

我不认为,很少会使用例外情况.但.

并非所有团队和项目都准备好使用例外.使用异常需要高级程序员资格,特殊技术以及缺乏大型遗留的非异常安全代码.如果你有庞大的旧代码库,那么它几乎总是不是异常安全的.我确定你不想重写它.

如果您要广泛使用例外,那么:

  • 准备好教你的人民什么是异常安全
  • 你不应该使用原始内存管理
  • 广泛使用RAII

另一方面,在具有强大团队的新项目中使用异常可能会使代码更清晰,更易于维护,甚至更快:

  • 你不会错过或忽略错误
  • 您不必编写返回代码检查,而不知道如何处理低级错误代码
  • 当你被迫编写异常安全的代码时,它变得更有条理


rlb*_*ond 6

并不是很少使用例外.只是他们应该只在特殊情况下抛出.例如,如果用户输入了错误的密码,那也不例外.

原因很简单:异常突然退出函数,并将堆栈向上传播到catch块.这个过程在计算上非常昂贵:C++构建其异常系统,以便在"普通"函数调用上获得很少的开销,因此当引发异常时,它必须做很多工作才能找到去处.而且,因为每行代码都可能引发异常.如果我们有一些f常常引发异常的函数,我们现在必须注意每次调用时使用try/ catchblocks f.这是一个非常糟糕的接口/实现耦合.

  • 你基本上重复了"他们被称为例外的理由"的理由.我正在寻找人们把它变成更实质的东西. (8认同)
  • "特殊情况"是什么意思?实际上,我的程序必须比糟糕的用户密码更频繁地处理实际的IOExceptions.这是否意味着您认为我*应该*使BadUserPassword成为异常,或者stdlib人*不应该使IOException成为异常?"计算成本非常高"不是真正的原因,因为我从未见过通过任何控制机制处理错误密码的程序(当然不是我的程序)是性能瓶颈. (4认同)

Ned*_*der 6

关于例外的所有经验法则都归结为主观术语.您不应该期望得到何时使用它们以及何时不使用它们的硬性和快速定义."只有在特殊情况下".好的循环定义:例外情况适用于特殊情况.

何时使用异常与"如何知道此代码是一类还是两类?"属于同一个桶.这部分是一个风格问题,部分是偏好.例外是一种工具.它们可以被使用和滥用,找到两者之间的界限是编程艺术和技巧的一部分.

有很多意见和权衡要做.找到与您说话的内容,并遵循它.


Nem*_*vic 5

我在一篇关于C++异常文章中提到了这个问题.

相关部分:

几乎总是,使用异常来影响"正常"流是一个坏主意.正如我们在3.1节中已经讨论过的,异常会生成不可见的代码路径.如果这些代码路径仅在错误处理方案中执行,则可以接受.但是,如果我们将异常用于任何其他目的,我们的"正常"代码执行将分为可见和不可见的部分,这使得代码很难阅读,理解和扩展.

  • 是的,但在推理错误代码时,我并没有神秘地得到任何聪明.因此,如果"隐形代码路径"很难阅读,理解并扩展为"普通"代码,那么我根本看不出为什么应用"错误"这个词使它们"可以接受".根据我的经验,错误情况一直发生,这使它们"正常".如果您可以在错误情况下找出异常,那么您可以在非错误情况下找出它们.如果你做不到,你就做不到,而你所做的一切都是让你的错误代码难以理解,但忽略了这个事实,因为它只是"错误". (2认同)

cop*_*pro 5

我的错误处理方法是有三种基本类型的错误:

  • 可以在错误站点处理的奇怪情况.这可能是用户在命令行提示符下输入无效输入.正确的行为只是向用户抱怨并在这种情况下循环.另一种情况可能是零除.这些情况并非真正的错误情况,通常是由输入错误引起的.
  • 类似于上一种情况,但在错误站点无法处理的情况.例如,如果您有一个采用文件名并使用该名称解析文件的函数,则可能无法打开该文件.在这种情况下,它无法处理错误.这是异常闪耀的时候.而不是使用C方法(将无效值作为标志返回并设置全局错误变量来指示问题),代码可以改为抛出异常.然后,调用代码将能够处理异常 - 例如,提示用户输入另一个文件名.
  • 一种不应该发生的情况.这是在违反类不变量或者函数接收到无效参数等的情况下.这表示代码中存在逻辑故障.根据故障级别,可能适用例外,或者强制立即终止可能是优选的(如同assert).通常,这些情况表明代码中的某些地方已经破坏了,并且您实际上无法相信任何其他内容是正确的 - 可能存在猖獗的内存损坏.你的船正在下沉,下车.

换句话说,例外情况是当您遇到可以处理的问题时,但是您无法在您注意到的地方处理.你无法解决的问题应该简单地杀死程序; 你可以立即处理的问题应该简单地处理.

  • 你在回答错误的问题.我们不想知道为什么我们应该(或不应该)考虑使用异常来处理错误场景 - 我们想知道为什么我们应该(或不应该)将它们用于非错误处理场景. (2认同)

小智 5

我在这里阅读了一些答案.我仍然对所有这些混乱感到惊讶.我强烈不同意所有这些异常== spagetty代码.我的意思是混淆,有些人不喜欢C++异常处理.我不确定我是如何学习C++异常处理的 - 但我在几分钟内理解了它的含义.这是在1996年左右,我使用的是用于OS/2的borland C++编译器.我决定何时使用异常从来没有问题.我通常将易错的do-undo操作包装到C++类中.这种撤消行动包括:

  • 创建/销毁系统句柄(用于文件,内存映射,WIN32 GUI句柄,套接字等)
  • 设置/取消设置处理程序
  • 分配/释放内存
  • 声明/释放互斥锁
  • 递增/递减引用计数
  • 显示/隐藏窗口

比有功能包装.将系统调用(不属于前一类)包装到C++中.例如,从/向文件读/写.如果某些内容失败,将抛出异常,其中包含有关错误的完整信息.

然后有捕获/重新抛出异常以向故障添加更多信息.

整体C++异常处理导致更清晰的代码.代码量急剧减少.最后,可以使用构造函数来分配错误的资源,并在发生此类故障后仍然保持无损坏的环境.

可以将这些类链接到复杂的类中.一旦某个成员/基础对象的构造函数被执行,就可以依赖于同一对象的所有其他构造函数(之前执行)成功执行.