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


80

我经常看到/听到人们说例外情况应该很少使用,而永远不要解释原因。尽管这可能是正确的,但基本原理通常是一个轻浮的说法:“由于某种原因它被称为例外”,在我看来,这似乎是一种受尊敬的程序员/工程师不应接受的解释。

有一系列问题可以使用异常来解决。为什么将它们用于控制流是不明智的?对它们的使用格外保守的背后的哲学是什么?语义学?性能?复杂?美学?惯例?

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

同样,我不一定不同意在特殊情况下应该保存它们,但是我想知道共识的基础是什么(如果存在这种情况)。



32
不能重复。链接的示例涉及异常处理是否完全有用。这是关于何时使用异常与何时使用其他错误报告机制之间的区别。
阿德里安·麦卡锡

1
以及这个stackoverflow.com/questions/1385172/…怎么样?
stijn

1
没有共识理由。对于抛出异常的“适当性”,不同的人有不同的意见,这些意见通常受他们所使用的语言的影响。您使用C ++标记了此问题,但是我怀疑如果使用Java标记此问题,则会得到不同的意见。
Charles Salvia

5
不,它不应该是维基。此处的答案需要专业知识,并应得到代表的奖励。
bobobobo 2010年

Answers:


91

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

另一个问题是,通常会抛出并吞下异常。开发人员使用此技术可以简单地使程序“静音”,并使其运行尽可能长的时间,直到完全崩溃为止。这是非常错误的。如果您不处理异常,如果您没有通过释放一些资源来做出适当的反应,或者您没有记录异常的发生或至少没有通知用户,那么您就没有使用异常的含义。

直接回答您的问题。很少使用例外,因为例外情况很少见,例外的代价很高。

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

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

这并不是说使用例外(微笑)不会有例外。有时,如果引发异常而不是通过多层转发返回代码,则可以简化代码结构。作为一条简单的规则,如果您希望某个方法经常被调用并且在一半的时间内发现某种“异常”情况,那么最好找到另一个解决方案。但是,如果您希望大多数时候都能正常运行,而这种“例外”情况只能在一些罕见的情况下出现,那么抛出异常就可以了。

@Comments:如果可以使代码更简单,更轻松,则可以在某些异常情况下使用异常。这个选项是开放的,但我会说它在实践中很少见。

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

因为异常会破坏正常的“控制流”。引发异常,程序的正常执行被放弃,有可能使对象处于不一致状态,并使某些开放资源无法释放。当然,C#具有using语句,该语句将确保即使从using主体抛出异常,也将处置该对象。但是,让我们暂时从语言中进行抽象。假设框架不会为您布置对象。您手动进行。您具有一些用于请求和释放资源和内存的系统。您在系统范围内有协议,由谁负责在什么情况下释放对象和资源。您有如何处理外部库的规则。如果程序遵循正常的操作流程,则效果很好。但是突然在执行过程中,您抛出了异常。一半的资源没有释放。尚未请求一半。如果该操作本来应该是事务性的,那么它将被破坏。您处理资源的规则将不起作用,因为负责释放资源的那些代码部分将无法执行。如果其他人想使用这些资源,他们可能会发现它们处于不一致状态并崩溃,因为他们无法预测这种特殊情况。

假设您希望方法M()调用方法N()进行一些工作并安排一些资源,然后将其返回给M(),后者将使用它并进行处理。精细。现在在N()中出了点问题,并在M()中引发了您没有想到的异常,因此该异常冒泡到顶部,直到它可能被某种方法C()捕获为止,该方法根本不知道到底发生了什么N()中的内容以及是否以及如何释放一些资源。

通过抛出异常,您可以创建一种使程序进入许多难以预测,难以理解和处理的新的不可预测的中间状态的方法。它有点类似于使用GOTO。设计一个可以随机将其执行从一个位置跳转到另一个位置的程序非常困难。也将很难维护和调试它。当程序变得越来越复杂时,您将失去对何时何地进行修复的更少了解。


5
如果您将引发异常的函数与返回错误代码的函数进行比较,则除非我缺少某些内容,否则堆栈展开将是相同的。
Catskul

9
@Catskul:不一定。函数返回直接将控制权返回给直接调用者。抛出的异常将控制权返回给整个当前调用堆栈中该异常类型(或其基类或“ ...”)的第一个捕获处理程序。
jon-hanson 09年

5
还应注意,调试器通常会在默认情况下中断异常。如果您在异常情况下使用异常,则调试器将中断很多。
2009年

5
@jon:只有当它未被捕获并需要转义更多作用域以进行错误处理时,才会发生这种情况。在相同的情况下,使用返回值(并且还需要向下传递多个作用域)将发生相同数量的堆栈展开。
Catskul

3
-1对不起。谈论异常设施是什么意思是毫无意义的。它们只是一种机制,可以用于处理内存不足等异常情况等。但这并不意味着它们也不应用于其他目的。我想看到的是为什么不应该将它们用于其他目的的解释。尽管您的答案确实只谈了一点,但它与很多有关“意味”异常的讨论混在一起。
j_random_hacker

61

尽管“在特殊情况下抛出异常”是一个很好的答案,但您实际上可以定义这些情况是什么:当满足先决条件而后继条件不能满足时。这使您可以编写更严格,更严格和更有用的后置条件,而无需牺牲错误处理;否则,您必须毫无例外地更改后置条件,以允许所有可能的错误状态。

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

建设者

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

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

僵尸的例子

此僵尸修改的一个示例是std :: ifstream,您必须始终检查其状态才能使用它。例如,因为std :: string不存在,所以始终保证您可以在构造后立即使用它。想象一下,如果您必须编写如本例这样的代码,并且如果忘记了检查僵尸状态,那么您要么默默地得到错误的结果,要么会破坏程序的其他部分:

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

甚至为该方法命名也是一个很好的例子,说明如何必须为情况字符串修改类的不变式和接口,因此无法预测或处理自身。

验证输入示例

我们来看一个常见的例子:验证用户输入。仅仅因为我们要允许失败的输入并不意味着解析函数需要将其包括在其后置条件中。但是,这确实意味着我们的处理程序需要检查解析器是否失败。

// 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';
}

不幸的是,这显示了两个示例,这些示例说明了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

前提条件

前提条件不必严格检查-违反前提条件总是表示逻辑失败,这是调用方的责任-但如果您检查了前提条件,则抛出异常是适当的。(在某些情况下,返回垃圾或使程序崩溃更合适;尽管这些动作在其他情况下可能是非常错误的。如何最好地处理未定义的行为是另一个主题。)

特别是,将stdlib异常层次结构的std :: logic_errorstd :: runtime_error分支进行对比。前者通常用于违反先决条件,而后者更适合于违反先决条件。


5
答案不错,但您基本上只是在说一条规则,而不是提供理由。那么您的理性是:惯例和风格吗?
Catskul

6
+1。这就是设计使用异常的方式,以这种方式使用异常实际上可以提高代码的可读性和可重用性。
Daniel Pryden 09年

15
Catskul:首先是特殊情况的定义,而不是基本原理或惯例。如果您不能保证后置条件,则不能返回。没有例外,您必须使后置条件极为广泛,以包括所有错误状态,以至于几乎没有用。看来我没有回答您的字面问题“为什么它们应该稀有”,因为我没有那样看待它们。它们是使我可以有效地收紧后置条件的工具,同时仍然允许发生错误(..在特殊情况下:)。

9
+1。前后条件是我所听过的最好的解释。
KitsuneYMG,2009年

6
许多人在说:“但这归结为惯例/语义。” 是啊,没错。但是约定和语义很重要,因为它们对复杂性,可用性和可维护性具有深远的影响。毕竟,是否正式定义前置条件和后置条件的决定还仅仅是约定和语义问题吗?但是,它们的使用可以使您的代码更加易于使用和维护。
Darryl 2010年

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

24
+1,总体上有充分的理由。但是请注意,堆栈展开和析构函数调用的成本(至少)是由任何功能上等效的错误处理机制支付的,例如,通过测试和返回错误代码(如C中
支付。– j_random_hacker 2009年

即使我同意j_random,也要将此标记为答案。在每个功能上等效的机制中,堆栈展开和资源取消/分配都是相同的。如果您同意,那么当您看到此内容时,只需将其从列表中剔除即可使答案更好。
Catskul

14
朱利安(Julien):j_random的评论指出没有相对的节省:int f() { char* s = malloc(...); if (some_func() == error) { free(s); return error; } ... }无论是手动执行还是通过异常执行,您都必须付出代价来释放堆栈。你不能比较使用异常没有错误处理可言

14
1.昂贵:过早的优化是万恶之源。2.难于分析:与嵌套的错误返回码层相比?我谨不同意。3.中间代码并不总是期望的:与嵌套层相比,不是总是合理地处理和翻译错误?新手都失败了。4.维护并发症:如何?由错误代码调解的依赖性是否更易于维护?...但这似乎是两所学校的开发人员难以接受彼此论点的领域之一。像其他任何东西一样,错误处理设计是一个折衷。
Pontus Gagge,2010年

1
我同意这是一个复杂的问题,很难用一般性来准确回答。但是请记住,最初的问题只是问为什么要给出反例外建议,而不是对总体最佳实践的解释。
DigitalRoss

22

在某种程度上,引发异常类似于goto语句。这样做是为了进行流控制,然后您将获得难以理解的意大利面条式代码。更糟糕的是,在某些情况下,您甚至不知道跳转的确切位置(即,如果您没有在给定的上下文中捕获异常)。这公然违反​​了增强可维护性的“最小惊喜”原则。


5
除了流控制以外,如何将异常用于其他目的?即使在特殊情况下,它们仍然是一种流控制机制,会对代码的清晰性产生影响(假设在这里:难以理解)。我想您可以使用例外,但可以使用ban catch,尽管在这种情况下,您最好还是自称std::terminate()。总体而言,在我看来,这种说法似乎是“从不使用异常”,而不是“仅很少使用异常”。
史蒂夫·杰索普

5
函数内部的return语句使您退出函数。异常带您知道不知道要增加多少层-没有简单的方法可以找出答案。

2
史蒂夫:我明白你的意思,但这不是我的意思。在意外情况下可以例外。有些人将其滥用为“早期回报”,甚至可能是某种转换声明。
Erich Kitzmueller 09年

2
我认为这个问题假设“意外”的解释不够充分。我在一定程度上同意。如果某些情况阻止某个功能按要求完成,则您的程序可以正确处理该情况,否则不能正确处理。如果能够处理,那么它是“预期的”,并且代码必须是可理解的。如果处理不正确,则很麻烦。除非让异常终止程序,否则代码的某些级别必须“期望”它。但是实际上,正如我在回答中所说的那样,尽管理论上并不令人满意,但这是一个好规则。
史蒂夫·杰索普

2
另外,包装器当然可以将抛出函数转换为返回错误的函数,反之亦然。因此,如果您的呼叫者不同意“意外”的想法,则不一定会破坏他们的代码可理解性。在他们激发许多您认为“意外”的条件的情况下,它可能会牺牲一些性能,但他们认为这是正常且可恢复的,因此会捕获并转换为errno或其他任何值。
史蒂夫·杰索普

16

异常使您更难以推理程序状态。例如,在C ++中,与不必使用某些函数相比,您必须做更多的思考以确保您的函数具有严格的异常安全性。

原因是没有例外,函数调用可以返回,也可以先终止程序。除异常外,函数调用可以返回,也可以终止程序,或者可以跳转到某个地方的catch块。因此,您不再仅仅通过查看您前面的代码就可以遵循控制流程。您需要知道调用的函数是否可以抛出。您可能需要知道可以引发什么以及在何处捕获它,这取决于您是否关心控件在哪里,还是只在乎它离开当前作用域。

因此,人们说“除非情况真的很特殊,否则不要使用例外”。当您开始使用它时,“真正的例外”表示“发生了一些情况,用错误的返回值处理它的好处被成本所抵消”。因此,是的,这是一个空洞的声明,尽管一旦您有了“真正的例外”的直觉,它就会成为一个好的经验法则。当人们谈论流控制时,它们意味着本地推理(不引用catch块)的能力是返回值的好处。

Java对“真正的例外”的定义比C ++更广泛。C ++程序员比Java程序员更想查看函数的返回值,因此在Java中,“真正的例外”可能意味着“由于该函数的结果,我不能返回非null对象”。在C ++中,它更可能表示“我非常怀疑我的调用者是否可以继续”。因此,如果Java流无法读取文件,则会抛出该异常,而C ++流(默认情况下)将返回一个指示错误的值。但是,在所有情况下,这都取决于您愿意强迫调用者编写什么代码。因此,这的确是一种编码风格的问题:您必须达成共识,代码应该是什么样子,以及要针对多少“异常安全性”编写多少“错误检查”代码

所有语言之间的广泛共识似乎是,就错误的可恢复性而言,这是最好的做法(因为不可恢复的错误导致没有异常的代码,但仍然需要检查并返回自己的代码,使用错误返回的代码中的错误)。因此人们开始期望“我调用的此函数引发异常”的意思是“无法继续”,而不仅仅是“不能继续”。这不是异常情况所固有的,它只是一个自定义,但像任何良好的编程习惯一样,它是一个聪明的人提倡的自定义,他们以另一种方式尝试了这种方法,但并不喜欢结果。因此,我个人认为确实是“非常例外”,除非有关情况使例外特别有吸引力。

顺便说一句,除了推理代码状态外,还涉及性能。现在,使用您有权关心性能的语言,异常通常很便宜。它们可以比多个级别的速度更快,“哦,结果是错误的,那么我也最好退出自己的错误”。在糟糕的过去,人们确实担心会抛出异常,捕捉异常并继续进行下一件事情,这会使您的工作变得如此缓慢以至无用。因此,在这种情况下,“真的非常”意味着“情况非常糟糕,以至于可怕的表现不再重要”。情况不再如此(尽管紧密循环中的异常仍然很明显),并希望指出为什么“真正例外”的定义需要灵活。


2
“例如,在C ++中,与不必使用某些函数相比,您必须做更多的思考以确保您的函数具有严格的异常安全性。” -鉴于基本的C ++构造(如new)和标准库都抛出了异常,所以我不明白在代码中不使用异常如何使您不必编写异常安全的代码。
帕维尔·米纳夫

1
您必须要问Google。但是,是的,要指出的是,如果您的函数没有抛出异常,那么您的调用者将不得不做一些工作,并且添加导致异常的额外条件不会使它“更多地引发异常”。但是无论如何,内存不足是一种特殊情况。通常有不处理它的程序,而只是关闭它。您可能有一个编码标准,其中说:“不要使用异常,不要提供异常保证,如果有新的抛出,那么我们将终止,并且我们将使用不会解开堆栈的编译器”。概念不高,但是会飞。
史蒂夫·杰索普

一旦使用了不解开堆栈的编译器(或者以其他方式禁用其中的异常),从技术上讲,它不再是C ++了:)
Pavel Minaev 2009年

我的意思是不会在未捕获的异常上释放堆栈。
史蒂夫·杰索普

除非它放开堆栈以找到捕获块,否则如何知道它未被捕获?
jmucchiello

11

确实没有达成共识。整个问题在某种程度上是主观的,因为通常由语言本身的标准库中的现有实践建议抛出异常的“适当性”。C ++标准库抛出异常的频率比说Java标准库要少得多,Java标准库几乎总是喜欢使用异常,即使对于预期的错误(例如无效的用户输入(例如Scanner.nextInt))也是如此。我认为,这将极大地影响开发人员对何时引发异常的意见。

作为一名C ++程序员,我个人更喜欢在非常“异常”的情况下保留异常,例如,内存不足,磁盘空间不足,启示录发生等。但是我不坚持认为这是绝对正确的方法东西。


2
我认为存在某种共识,但也许共识更多地基于惯例而不是合理的推理。仅在特殊情况下使用异常可能是有充分的理由的,但是大多数开发人员并没有真正意识到原因。
Qwertie

4
同意-您经常会听到“例外情况是针对特殊情况的”,但是没有人会去适当地定义“例外情况”是什么-它主要是自定义的,并且肯定是特定于语言的。哎呀,在Python中,迭代器使用异常来表示序列结束,这被认为是完全正常的!
帕维尔米纳夫

2
+1。没有严格的规则,只有约定-但是遵守使用您的语言的其他人的约定很有用,因为它使程序员更容易理解彼此的代码。
j_random_hacker

1
“启示录发生了”-没关系,如果有什么理由证明UB,那就例外;-)
史蒂夫·杰索普

3
Catskul:几乎所有编程都是惯例。从技术上讲,我们甚至不需要例外,甚至根本不需要例外。如果不涉及NP完整性,big-O / theta / little-o或Universal Turing Machines,则可能是惯例。:-)
肯(Ken)

7

我认为,应该很少使用例外。但。

并非所有团队和项目都准备好使用异常。异常的使用要求程序员具有很高的资格,需要特殊技术,并且缺少大量的传统的非异常安全代码。如果您拥有庞大的旧代码库,那么它几乎总是不安全的。我确定您不想重写它。

如果要广泛使用异常,则:

  • 准备教导您的员工什么是例外安全
  • 您不应该使用原始内存管理
  • 广泛使用RAII

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

  • 您不会错过或忽略错误
  • 您不必编写返回代码检查,而无需真正知道在底层使用错误代码该怎么办
  • 当您被迫编写异常安全代码时,它变得更加结构化

1
我特别喜欢您提到“并非所有团队……都准备使用例外”这一事实。异常肯定看起来很容易实现,但是很难做到正确,这是使它们变得危险的部分原因。
j_random_hacker

1
+1表示“当您被迫编写异常安全代码时,它变得更加结构化”。基于异常的代码被迫具有更多的结构,而且我发现在无法忽略对象和不变量的情况下,实际上更容易推理。实际上,我相信强大的异常安全性就是编写几乎可逆的代码,这使得避免不确定状态变得非常容易。
汤姆

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();
    }
}

对我来说,这段代码很荒谬。当然可以。没有人对此争论。但这应该是这样的:

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

while (!int.TryParse(input, out value)) {
    input = GetInput();
}

示例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();
    }
}

这似乎足够合理,对吧?我们正在尝试打开文件;如果不存在,我们将捕获该异常并尝试打开其他文件...这是怎么回事?

真的没什么。但是考虑一下这种替代方案,它不会引发任何异常:

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();
    }
}

当然,如果这两种方法的性能实际上相同,那么这实际上将纯粹是一个理论问题。因此,让我们看一下。对于第一个代码示例,我列出了10000个随机字符串的列表,其中没有一个表示正确的整数,然后在最后添加一个有效的整数字符串。使用以上两种方法,这些都是我的结果:

使用try/catch封锁:25.455
使用int.TryParse:1.637毫秒

对于第二个示例,我基本上执行了相同的操作:列出了10000个随机字符串的列表,这些都不是有效路径,然后在最后添加一个有效路径。结果是:

使用try/catch阻止: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;
    }
}

我敢打赌,如果您对此功能进行了概要分析,您会发现它对于典型的业务应用程序的执行速度相当快。这并没有改变这样一个事实,那就是这是一种效率低下的方法,无法完成可以做得更好的事情。

这就是人们谈论异常“滥用”时的意思。


2
“异常处理的性能令人恐怖。” -这是一个实现细节,并非对所有语言都适用,甚至对于所有C ++也不适用。
帕维尔·米纳夫

“这是一个实现细节”,我不记得我多久听到一次这种争论,而且它只是发臭。请记住:所有计算机内容都是有关实现细节的。并且:用螺丝刀代替锤子是不明智的,因为人们通常会谈论“只是实施细节”。
根,

2
然而,Python很高兴地将异常用于通用流控制,因为对其实现的性能影响并不算差。因此,如果您的锤子看起来像螺丝刀,那么,请怪罪于锤子...
Pavel Minaev 09年

1
@j_random_hacker:是的,GetNine它的工作很好。我的观点是没有理由使用它。这并不是完成工作的“快速而简单”的方法,而更“正确”的方法将需要更多的精力。以我的经验,通常情况下,“正确”的方法实际上会花费更少的精力,或者差不多。无论如何,对我而言,性能是不鼓励使用左右异常的唯一客观原因。至于性能损失是否被“严重高估”,这实际上是主观的。
丹涛

1
@j_random_hacker:旁白是非常有趣的,并且确实使Pavel意识到实现取决于性能的观点。我非常怀疑的一件事是,有人会发现这样一种情况:在使用例外情况下,实际情况要比替代方案要好(至少存在替代方案,例如在我的示例中)。
丹涛

6

关于例外的所有经验法则都归结为主观术语。您不应期望对何时使用它们以及何时不使用它们进行严格的定义。“仅在特殊情况下”。好的循环定义:例外是针对特殊情况。

何时使用异常与“如何知道此代码是一类还是两类?”属于同一类。这部分是风格问题,部分是偏爱。例外是一种工具。它们可以被使用和滥用,并且找到两者之间的界线是编程技术和技巧的一部分。

有很多意见,需要权衡。找到可以和您说话的内容,然后按照它进行。


6

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

原因很简单:异常会突然退出函数,并沿堆栈向上传播到一个catch块。此过程在计算上非常昂贵:C ++构建其异常系统时,对“常规”函数调用的开销很小,因此,当引发异常时,它必须做大量工作才能找到要去的地方。而且,由于每一行代码都可能引发异常。如果我们有一些f经常引发异常的函数,那么我们现在必须注意在每次调用时都使用try/catchf。这是一个非常糟糕的接口/实现耦合。


8
从本质上讲,您已经重复了“由于某种原因将它们称为异常”的原理。我正在寻找人们将其充实为更实质的东西。
Catskul

4
“特殊情况”是什么意思?实际上,我的程序必须比错误的用户密码更频繁地处理实际的IOException。这是否意味着您认为我应该使BadUserPassword成为异常,或者说stdlib伙计们应该使IOException成为异常?“在计算上非常昂贵”不可能是实际原因,因为我从未见过一个程序(当然不是我的)通过任何控制机制来处理错误密码的程序是性能瓶颈。
肯(Ken)

5

我在有关C ++异常文章中提到了此问题。

相关部分:

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


1
您看,我从C来到C ++,并且我认为“错误处理方案”是正常的。它们可能不会被频繁执行,但是与非错误代码路径相比,我花了更多时间在思考它们。因此,我再次大体上同意这种观点,但是这并不能使我们更接近定义“正常”,以及如何从“正常”的定义中推断出使用异常是一个不好的选择。
史蒂夫·杰索普

我也是从C来的C ++,但是即使在CI中,“正常”流结束错误处理也有所区别。如果调用ie fopen(),则存在一个分支,它处理成功案例并且是“正常”的,而一个分支则处理失败的可能原因,并且是错误处理。
Nemanja Trifunovic,

2
是的,但是在推理错误代码时,我不会变得神秘。因此,如果很难理解,理解和扩展“普通”代码的“不可见代码路径”,那么我根本看不到为什么使用“错误”一词会使它们“可以接受”。根据我的经验,错误情况一直在发生,这使它们成为“正常”情况。如果您可以在错误情况下找出异常,那么可以在非错误情况下找出它们。如果不能,那么就不能,而且您所做的所有事情都会使您的错误代码变得难以理解,但由于“仅是错误”而忽略了这一事实。
史蒂夫·杰索普

1
示例:您有一个需要完成某些工作的函数,但是您将尝试各种方法的全部负载,并且您不知道哪种方法会成功,并且每个方法都可能会尝试一些额外的子方法,并且以此类推,直到有效果为止。然后,我认为失败是“正常的”,成功是“例外的”,尽管显然不是错误。在大多数程序中,成功抛出异常并不比在可怕的错误中抛出异常更难理解-您确保只有一点点代码需要知道下一步该做什么,然后直接跳到那里。
史蒂夫·杰索普

1
@onebyone:您的第二条评论实际上是我在其他地方未曾看到过的观点。我认为,这样做的答案是(正如您在自己的答案中所说的那样)使用错误条件的异常已被许多语言的“标准做法”所吸收,即使是最初的原因,也应遵守该准则。这样做被误导了。
j_random_hacker

5

我对错误进行处理的方法是存在三种基本错误类型:

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

解释一下,例外是当您遇到问题时可以解决的,但无法在发现问题的地方解决。您无法处理的问题应该简单地杀死程序;您可以立即处理的问题应该得到处理。


2
您在回答错误的问题。我们不想知道为什么我们应该(或不应该)考虑使用异常来处理错误情况-我们想知道为什么我们应该(或不应该)将它们用于非错误处理情况。
j_random_hacker

5

我在这里阅读了一些答案。我仍然对所有这些困惑感到惊讶。我非常不同意所有这些exceptions == spagetty代码。混乱是指有些人不欣赏C ++异常处理。我不确定如何了解C ++异常处理-但我在几分钟内就了解了其中的含义。那是在1996年左右,当时我在使用OS / 2的borland C ++编译器。我从来没有什么问题可以决定何时使用异常。我通常将容易犯错的撤消操作包装到C ++类中。此类撤消操作包括:

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

比功能包装还多。将系统调用(不属于前一类)包装到C ++中。例如从文件读/写。如果失败,将引发异常,其中包含有关错误的完整信息。

然后是捕获/抛出异常,以向故障添加更多信息。

总体而言,C ++异常处理将导致更清晰的代码。大大减少了代码量。最终,人们可以使用构造函数来分配易变的资源,并且在发生此类故障后仍可以保持无损坏的环境。

可以将这样的类链接为复杂的类。一旦执行了某个成员/基础对象的构造函数,就可以依靠该对象(之前执行)的所有其他构造函数成功执行。


3

与传统构造(循环,if,函数等)相比,异常是一种非常不寻常的流控制方法。常规控制流构造(循环,if,函数调用等)可以处理所有正常情况。如果发现例行事件会导致异常,那么也许您需要考虑代码的结构。

但是,某些类型的错误无法通过常规构造轻松解决。灾难性故障(例如资源分配故障)可以在较低的级别上检测到,但可能无法在此处进行处理,因此,简单的if语句不足。这些类型的故障通常需要在更高级别上进行处理(例如,保存文件,记录错误,退出)。试图通过传统方法(如返回值)报告这样的错误既繁琐又容易出错。此外,它将开销投入到中级API层中,以处理这种奇怪的异常故障。开销分散了这些API的客户端,并要求它们担心超出其控制范围的问题。异常提供了一种对大错误进行非本地处理的方法

如果客户端ParseInt使用字符串进行调用,并且该字符串不包含整数,则直接调用者可能会关心该错误并知道如何处理。因此,您可以将ParseInt设计为返回诸如此类的失败代码。

另一方面,如果ParseInt由于由于内存碎片严重而无法分配缓冲区而失败,则调用者将不知道该如何处理。它将不得不冒这个不寻常的错误,直到处理这些基本故障的某个层次。这会给介于两者之间的每个人增加负担(因为他们必须在自己的API中容纳错误传递机制)。使用异常可以跳过这些层(同时仍确保进行必要的清理)。

在编写低级代码时,可能很难决定何时使用传统方法以及何时引发异常。低级代码必须做出决定(是否抛出)。但是,真正了解期望值和例外情况的是更高级别的代码。


1
但是,在Java中,parseInt实际上确实会引发异常。因此,我想说的是,有关引发异常的适当性的意见高度依赖于其选择的开发语言的标准库中的现有做法。
Charles Salvia

Java的运行时无论如何都必须跟踪信息,这样就可以很容易地生成异常... c ++代码往往手头没有这些信息,因此生成它会降低性能。通过不随时使用它,它往往会变得更快/更小/更便于缓存。等等。此外,Java代码具有对数组等内容的动态检查,这是c ++所不具有的功能,因此java异常是开发人员所特有的已经用try / catch块处理了很多这些事情,那么为什么不对所有东西使用exceptons呢?
Ape-in​​ago

1
@Charles-这个问题是用C ++标记的,所以我从那个角度回答。我从未听过Java(或C#)程序员说异常仅适用于特殊情况。C ++程序员经常这样说,而问题是为什么。
Adrian McCarthy

1
实际上,C#异常确实也这么说,原因是在.NET中抛出异常非常昂贵(比Java中的异常还要多)。
帕维尔米纳夫

1
@Adrian:是的,这个问题是用C ++标记的,尽管异常处理哲学在某种程度上是一种跨语言对话,因为语言之间肯定会相互影响。无论如何,Boost.lexical_cast引发异常。但是,无效字符串真的那么“例外”吗?可能不是,但是Boost开发人员认为拥有出色的转换语法值得使用异常。这只是说明整个事情是多么主观。
Charles Salvia

3

在C ++中有几个原因。

首先,通常很难看到异常的来源(因为它们几乎可以从任何东西中抛出),因此catch块是COME FROM语句的一部分。它比GO TO更糟糕,因为在GO TO中,您知道您来自哪里(该语句,而不是一些随机函数调用)以及您要到达的位置(标签)。它们基本上是C的setjmp()和longjmp()的潜在资源安全版本,没有人愿意使用它们。

其次,C ++没有内置垃圾回收,因此拥有资源的C ++类在其析构函数中将其清除。因此,在C ++异常处理中,系统必须在范围内运行所有析构函数。在没有GC且没有真正的构造函数的语言(如Java)中,抛出异常的负担要轻得多。

第三,C ++社区,包括Bjarne Stroustrup和标准委员会以及各种编译器作者,一直在假设例外应该是例外。一般来说,与语言文化相抵触是不值得的。这些实现基于例外将很少发生的假设。更好的书将例外视为例外。好的源代码很少使用例外。优秀的C ++开发人员将异常视为例外。与此相反,您需要一个很好的理由,而我所看到的所有理由都是为了使其与众不同。


1
“ C ++没有内置垃圾回收,因此拥有资源的C ++类在其析构函数中将其清除。因此,在C ++异常处理中,系统必须在范围内运行所有析构函数。在具有GC且没有实际构造函数的语言中像Java一样,抛出异常的负担要轻得多。” -正常离开范围时必须运行析构函数,并且Javafinally块与C ++析构函数在实现开销方面没有任何不同。
帕维尔米纳夫

我喜欢这个答案,但是,像帕维尔一样,我认为第二点不是合法的。当范围由于某种原因而终止(包括其他类型的错误处理或仅继续执行程序)时,无论如何都将调用析构函数。
Catskul

+1表示语言文化是顺其自然的原因。这个答案使某些人不满意,但这是一个真正的原因(我相信这是最准确的原因)。
j_random_hacker

@Pavel:请忽略我上面(现在已删除)的错误注释,即finally不能保证运行Java块-当然可以。我对finalize()不能保证运行的Java方法感到困惑。
j_random_hacker

回顾这个答案,析构函数的问题在于必须立即调用它们,而不是在每次函数返回时都将它们分隔开。那仍然不是一个很好的理由,但我认为它有一点点有效性。
David Thornley 2010年

2

这是一个使用异常作为控制流的错误示例:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   try {
      totalIncome= calculateIncomeAsTypeA();
   } catch (IncorrectIncomeTypeException& e) {
      totalIncome= calculateIncomeAsTypeB();
   }

   return totalIncome;
}

这很不好,但是您应该写:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   if (incomeType == A) {
      totalIncome= calculateIncomeAsTypeA();
   } else if (incomeType == B) {
      totalIncome= calculateIncomeAsTypeB();
   }
   return totalIncome;
}

第二个示例显然需要一些重构(例如使用设计模式策略),但是很好地说明了异常并不意味着控制流。

异常还会带来一些性能损失,但是性能问题应遵循以下规则:“过早的优化是万恶之源”


看起来情况是,多态性更好,而不是check incomeType
Sarah Vessels,2009年

@Sarah是的,我知道那会很好。它仅在此处用于说明目的。
Edison Gustavo Muenz 09年

3
为什么不好?为什么要用第二种方式写?询问者想知道规则的原因,而不是规则。-1。
j_random_hacker

2
  1. 可维护性:正如上面的人所提到的,一掷千金就类似于使用goto。
  2. 互操作性:如果使用异常,则无法将C ++库与C / Python模块连接(至少不容易)。
  3. 性能降低:RTTI用于实际查找施加额外开销的异常类型。因此,异常不适用于处理常见的用例(用户输入int而不是字符串等)。

2
1.但是在某些语言中,例外情况一言以蔽之。3.有时性能并不重要。(80%的时间?)
UncleBens

2
2. C ++程序几乎总是使用异常,因为很多标准库都使用它们。容器类抛出。字符串类抛出。流类抛出。
David Thornley,2009年

除之外,还会抛出什么容器和字符串操作at()?就是说,任何人new都可以扔,所以……
Pavel Minaev 09年

据我了解,对于尝试/抛出/捕获,RTTI并非绝对必要。我一直都提到性能下降,我相信,但是没有人提到规模或链接到任何参考资料。
Catskul

UncleBens:(1)用于维护性而非性能。尝试/捕获/抛出过多的使用会降低代码的可读性。经验法则(我可能为此大怒,但这只是我的看法),进入和退出点越少,阅读代码就越容易。David Thornley:不需要互操作性时。当您编译从C代码调用的库时,gcc中的-fno-exceptions标志将禁用异常。
Sridhar Iyer

2

我要说的是,异常是一种以安全的方式使您脱离当前上下文(从最简单的意义上来说,脱离当前堆栈框架,但不止于此)的机制。这是结构化编程最接近goto的东西。要按预期使用异常的方式使用异常,您必须遇到一种情况,即您无法继续您现在正在做的事情,并且无法在当下的时刻处理它。因此,例如,当用户密码错误时,可以通过返回false继续操作。但是,如果UI子系统报告它甚至无法提示用户,则仅返回“登录失败”将是错误的。当前的代码级别根本不知道该怎么办。因此,它使用异常机制将责任委派给可能知道该做什么的人。


2

一个非常实际的原因是,在调试程序时,我经常打开First Chance Exceptions(调试-> Exceptions)来调试应用程序。如果发生许多异常,则很难找到发生了“错误”的地方。

此外,它还导致了一些反模式,例如臭名昭​​著的“接球”,并掩盖了实际问题。有关这方面的更多信息,请参阅我对此主题发表博客文章


2

我更喜欢尽可能少地使用异常。异常迫使开发人员处理可能不是真实错误的某些情况。所讨论的异常是致命问题还是必须立即处理的问题的定义。

与之相反的观点是,懒惰的人只需要打字就可以自己开枪。

Google的编码政策规定,切勿使用异常,尤其是在C ++中。您的应用程序要么不准备处理异常,要么准备就绪。如果不是,那么异常可能会传播到应用程序死亡。

找出一些您曾经使用过的throws异常并且您还没有准备好处理它们的库,这从来都不是一件有趣的事情。


1

合法案例引发异常:

  • 您尝试打开一个文件,该文件不存在,抛出FileNotFoundException;

非法案例:

  • 您只想在文件不存在时执行某些操作,然后尝试打开该文件,然后将一些代码添加到catch块中。

当我想将应用程序的流程分解到一定程度时,会使用异常。这是该异常的catch(...)所在的位置。例如,很常见,我们必须处理大量项目,并且每个项目都应独立于其他项目进行处理。因此,处理项目的循环具有一个try ... catch块,并且如果在项目处理过程中引发了某些异常,则该项目的所有内容都会回滚,记录错误,并处理下一个项目。生活仍在继续。

我认为您应该对不存在的文件,无效的表达式以及类似的东西使用异常。 如果有简单/便宜的替代方法,则不应在范围测试/数据类型测试/文件存在/其他情况下使用异常。 如果有简便/廉价的替代方案,则不应在范围测试/数据类型测试/文件存在/其他情况下使用异常,因为这种逻辑会使代码难以理解:

RecordIterator<MyObject> ri = createRecordIterator();
try {
   MyObject myobject = ri.next();
} catch(NoSuchElement exception) {
   // Object doesn't exist, will create it
}

这样会更好:

RecordIterator<MyObject> ri = createRecordIterator();
if (ri.hasNext()) {
   // It exists! 
   MyObject myobject = ri.next();
} else {
   // Object doesn't exist, will create it
}

添加到答案的评论:

也许我的示例不是很好-ri.next()在第二个示例中不应引发异常,如果发生异常,则确实存在一些异常,应在其他地方采取其他措施。当大量使用示例1时,开发人员将捕获通用异常而不是特定异常,并假定该异常是由于他们所期望的错误,但也可能是由于其他原因。最后,这导致实际异常被忽略,因为异常成为应用程序流的一部分,而不是应用程序流的一部分。

对此的评论可能会比我的回答本身更多。


1
为什么不?为什么不针对您提到的第二种情况使用例外?询问者想知道规则的原因,而不是规则。
j_random_hacker

感谢您的详细说明,但是恕我直言,您的2个代码段具有几乎相同的复杂性-都使用高度本地化的控制逻辑。当您在try块中有很多语句时,异常的复杂性最明显地超过了它/当时/其他的复杂性,其中任何一条都可能引发-您同意吗?
j_random_hacker

也许我的示例不是很好-ri.next()在第二个示例中不应引发异常,如果发生异常,则确实存在一些异常,应在其他地方采取其他措施。当大量使用示例1时,开发人员将捕获一个通用异常而不是特定异常,并假定该异常是由于他们所期望的错误,但也可能是由于其他原因。最后,这导致实际异常被忽略,因为异常成为应用程序流的一部分,而不是应用程序流的一部分。
拉维·瓦劳

因此,您说的是:随着时间的流逝,其他语句可能会在try块内累积,然后您就无法再确定该catch块确实在捕获您认为正在捕获的东西了吗?虽然很难以相同的方式滥用if / then / else方法,因为您一次只能测试一件事,而不能一次测试一组事情,因此异常方法会导致代码更加脆弱。如果是这样,请在您的答案中进行讨论,我会很乐意+1,因为我认为代码脆弱性是善意的原因。
j_random_hacker

0

基本上,异常是一种非结构化且难以理解的流控制形式。当处理不属于正常程序流的错误条件时,这是必要的,以避免错误处理逻辑使代码的正常流控制变得过于混乱。

如果要提供合理的默认值,以防调用方忽略编写错误处理代码,或者与直接调用方相比,最好在调用堆栈中进一步处理错误,则应使用IMHO异常。明智的默认设置是使用合理的诊断错误消息退出程序。疯狂的选择是程序在错误的状态下蠕动并在以后崩溃或无提示地产生不好的输出,难以诊断。如果“错误”足以作为程序流程的正常部分,以至于调用者无法合理地忘记对其进行检查,则不应使用异常。


0

我认为“很少使用”不是正确的句子。我希望“仅在特殊情况下才扔”。

许多人已经解释了为什么在正常情况下不应使用异常。异常具有处理错误的权利,并且仅具有处理错误的权利。

我将重点放在另一点:

另一件事是性能问题。编译器为使它们快速运行而竭尽全力。我不确定现在的确切状态,但是当您将异常用于控制流时,您会遇到另一个麻烦:您的程序将变慢!

原因是,异常不仅是非常强大的goto语句,而且它们还必须释放堆栈中所有离开的帧。因此,隐式地,堆栈上的对象也必须被解构,依此类推。因此,如果不知道这一点,那么抛出一个异常确实会涉及到很多机制。处理器将不得不做很多事情。

因此,您最终将在不知不觉中优雅地刻录处理器。

因此:仅在例外情况下使用例外-含义:当发生真正的错误时!


1
“原因是,异常不仅是非常强大的goto语句,它们还必须解开它们所离开的所有帧的堆栈。因此,隐式地,堆栈上的对象也必须被解构,依此类推。” -如果您在调用堆栈上掉了几个堆栈帧,那么所有这些堆栈帧(及其上的对象)最终都将被破坏-发生这种情况无关紧要,因为您返回正常,或者抛出了例外。最后,除了调用之外std::terminate(),您仍然必须销毁。
帕维尔米纳夫

你当然是对的。但仍然请记住:每次使用异常时,系统都必须提供基础设施来神奇地完成所有这些工作。我只是想稍微说明一下,这不仅是goto。同样在放卷时,系统必须找到正确的接球位置,这将花费额外的时间,代码和使用RTTI。因此,这不仅仅是跳跃-大多数人都不知道这点。

只要您坚持使用符合标准的C ++,RTTI就不可避免。否则,当然会有开销。只是我经常看到它被夸大了。
帕维尔·米纳夫

1
-1。“异常具有处理错误的权利,而纯粹具有处理错误的权利”-谁说?为什么?性能不是唯一的原因。(A)世界上大多数代码都不在游戏渲染引擎或矩阵乘法功能的最内层循环中,因此使用异常不会有明显的性能差异。(B)正如一个人在某处的评论中指出的,即使旧的C样式的错误返回值检查并在需要时传回错误,最终所有这些堆栈展开都将最终发生。使用处理方法。
j_random_hacker

说我。在此主题中,引用了很多原因。只是读他们。我只想描述一个。如果不是您需要的那个,请不要怪我。
根,

0

例外的目的是使软件具有容错能力。但是,必须对函数抛出的每个异常提供响应会导致抑制。异常只是一个正式的结构,迫使程序员承认例程可能会出错,并且客户端程序员需要了解这些条件并在必要时予以满足。

坦白地说,异常是对编程语言的一种沉迷,它向开发人员提供了一些正式的要求,这些要求将处理错误案例的责任从直接开发人员转移到了将来的开发人员。

我相信,一种好的编程语言不支持C ++和Java中的异常。您应该选择可以为函数的各种返回值提供替代流程的编程语言。程序员应该负责预期例程的所有形式的输出,如果可以的话,应将它们处理在单独的代码文件中。


0

如果出现以下情况,我将使用例外:

  • 发生了无法从本地AND恢复的错误
  • 如果无法从程序中恢复错误,则应终止。

如果错误可以从中恢复(用户输入的是“ apple”而不是数字),则可以恢复(再次询问输入,更改为默认值,等等)。

如果无法从本地恢复错误,但应用程序可以继续(用户尝试打开文件,但该文件不存在),则错误代码是适当的。

如果无法从本地恢复错误,并且应用程序无法恢复而无法继续运行(您的内存/磁盘空间不足等),那么采取异常处理是正确的方法。


1
-1。请仔细阅读问题。大多数程序员要么认为异常适用于某些类型的错误处理,要么就不适当-他们甚至不考虑将其用于其他更奇特的流控制形式的可能性。问题是:为什么呢?
j_random_hacker

您还应该仔细阅读。我回答说:“如何保守使用它们背后的哲学是什么?” 我的理念是保守使用它们。
条例草案

恕我直言,您没有解释为什么保守主义是必要的。为什么有时它们只“合适”?为什么不一直?(顺便说一句,我认为您建议的方法很好,或多或少是我自己做的,我只是认为这并
不能解决

OP提出了七个不同的问题。我选择只回答一个。很抱歉,您认为这值得一票。
法案

0

谁说应该保守使用?只是永远不要将异常用于流控制,仅此而已。谁在乎已经抛出异常的代价?


0

我的两分钱:

我喜欢使用异常,因为它使我可以编程,好像不会发生任何错误。因此,我的代码保持可读性,不会因各种错误处理而分散。当然,错误处理(异常处理)被移到末尾(catch块),或者被认为是调用级别的责任。

对我来说,一个很好的例子是文件处理或数据库处理。假设一切正常,然后在最后或发生某些异常时关闭文件。或在发生异常时回滚您的事务。

例外的问题是,它很快变得非常冗长。虽然它旨在使您的代码保持高度可读性,并且只专注于正常的事物流,但是如果使用得当,几乎每个函数调用都需要包装在try / catch块中,这会破坏目的。

对于前面提到的ParseInt,我喜欢例外的想法。只需返回值即可。如果参数不可解析,则引发异常。一方面,它使您的代码更整洁。在呼叫级别,您需要执行以下操作

try 
{
   b = ParseInt(some_read_string);
} 
catch (ParseIntException &e)
{
   // use some default value instead
   b = 0;
}

代码是干净的。当我像这样散布所有的ParseInt时,我使包装函数处理异常并返回默认值。例如

int ParseIntWithDefault(String stringToConvert, int default_value=0)
{
   int result = default_value;
   try
   {
     result = ParseInt(stringToConvert);
   }
   catch (ParseIntException &e) {}

   return result;
}

因此可以得出结论:在整个讨论过程中,我错过了一个事实,那就是异常使我可以使我的代码更容易/更具可读性,因为我可以更多地忽略错误条件。问题:

  • 例外仍然需要在某个地方处理。额外的问题:c ++没有允许其指定函数可能抛出哪些异常的语法(就像java一样)。因此,调用级别不知道可能需要处理哪些异常。
  • 如果每个函数都需要包装在try / catch块中,则有时代码会变得非常冗长。但这有时还是有道理的。

因此,有时很难找到一个良好的平衡。


-1

很抱歉,答案是“由于某种原因,它们被称为例外”。这种解释是“经验法则”。您无法给出应该使用或不应该使用异常的完整情况集,因为针对一个问题域的致命异常(英语定义)是针对不同问题域的正常操作过程。经验法则不能盲目遵循。相反,它们旨在指导您对解决方案进行调查。“将它们称为异常是有原因的”告诉您,应提前确定调用者可以处理的正常错误以及在没有特殊编码(捕获块)的情况下调用者无法处理的异常情况。

几乎每个编程规则实际上都是一个准则,上面写着“除非有充分的理由,否则请不要这样做”:“切勿使用goto”,“避免使用全局变量”,“正则表达式将问题数量预先增加一个”等。例外也不例外。...


...询问者想知道为什么这是一个经验法则,而不是(再次)听说这是经验法则。-1。
j_random_hacker

我在回应中承认了这一点。没有明确的原因。根据定义,经验法则含糊不清。如果有明确的原因,那将是一条规则而不是经验法则。上面每隔一个答案中的每一个解释都包含警告,因此他们也不解释为什么。
jmucchiello,2009年

可能没有明确的“为什么”,但是有其他人提到的部分“原因”,例如“因为这就是其他人正在做的事情”(恕我直言,一个可悲但真实的原因)或“表现”(恕我直言,这个原因通常被夸大了) ,但这仍然是一个原因)。其他经验法则也是如此,例如避免goto(通常会使控制流分析复杂得多,而不是等效循环)和避免全局变量(它们可能引入大量耦合,使以后的代码更改变得困难,并且通常是相同的)可以通过减少其他方式的耦合来实现目标)。
j_random_hacker

所有这些原因都有很多警告,这就是我的答案。除了在编程方面的丰富经验外,没有真正的原因。有一些经验法则超出了为什么。相同的经验法则可能会导致“专家”对原因持不同意见。您自己会带着一粒盐来“表演”。那将是我的首要任务。流量控制分析甚至都没有向我注册,因为(例如“ no goto”)我发现流量控制问题被夸大了。我还将指出,我的答案试图解释您如何使用“ trite”答案。
jmucchiello,2009年

我同意您的所有经验法则,但有很多警告。我不同意的是,我认为有必要尝试找出该规则的原始原因以及具体的警告。(我的意思是,在理想的世界中,我们有无限的时间去思考这些事情,当然也没有最后期限;)我认为这就是OP所要求的。
j_random_hacker
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.