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

时间:2009-11-16 18:46:27

标签: c++ exception

我经常看到/听到人们说异常应该很少使用,但从不解释原因。虽然这可能是正确的,但理由通常是一种瑕疵:“它被称为例外的原因”对我而言,似乎是一种不应被一位受人尊敬的程序员接受的解释/工程师。

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

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

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

28 个答案:

答案 0 :(得分:88)

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

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

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

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

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

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

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

  

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

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

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

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

答案 1 :(得分:61)

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

    在调用函数之前,
  • 前提条件必须为true
  • 后置条件 之后
  • 异常安全性说明异常如何影响函数或数据结构的内部一致性,并经常处理从外部传入的行为(例如仿函数,模板参数的ctor等)。 / LI>

构造

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

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

Zombie示例

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

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

即使命名该方法也是一个很好的例子,说明必须如何修改类 string 的类的不变性和接口,既不能预测也不能自行处理。

验证输入示例

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

// 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

前提条件

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

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

答案 2 :(得分:39)

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

答案 3 :(得分:22)

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

答案 4 :(得分:16)

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

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

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

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

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

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

答案 5 :(得分:11)

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

作为一名C ++程序员,我个人更喜欢为非常“特殊”的情况保留例外,例如:内存不足,磁盘空间不足,天启发生等等。但我并不坚持认为这是绝对正确的做事方式。

答案 6 :(得分:7)

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

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

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

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

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

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

答案 7 :(得分:7)

答案 8 :(得分:6)

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

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

答案 9 :(得分:6)

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

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

有很多意见和权衡取舍。找一些与你说话的东西,然后按照它。

答案 10 :(得分:5)

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

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

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

然后有捕获/重新抛出异常以向失败添加更多信息。

整体C ++异常处理可以带来更干净的代码。 代码量急剧减少。 最后,可以使用构造函数来分配易错资源,并在发生此类故障后仍然保持无腐败的环境。

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

答案 11 :(得分:5)

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

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

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

答案 12 :(得分:5)

我在article on C++ exceptions中提到了这个问题。

相关部分:

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

答案 13 :(得分:3)

与传统结构(循环,ifs,函数等)相比,异常是一种非常不寻常的流控制方法。正常的控制流构造(循环,ifs,函数调用等)可以处理所有正常情况。如果您发现自己在例行事件中遇到异常,那么您可能需要考虑代码的结构。

但是有些类型的错误使用普通结构无法轻松处理。灾难性故障(如资源分配失败)可以在较低级别检测到,但可能无法在那里处理,因此简单的if语句是不合适的。通常需要在更高级别处理这些类型的故障(例如,保存文件,记录错误,退出)。尝试通过传统方法(如返回值)报告这样的错误是单调乏味且容易出错的。此外,它将开销注入到中级API的层中,以处理这种奇怪的,异常的故障。开销分散了这些API的客户端,并要求他们担心无法控制的问题。例外提供了一种对大错误进行非本地处理的方法,这些错误在检测问题和处理程序之间对所有层都是不可见的。

如果客户端使用字符串调用ParseInt,并且字符串不包含整数,则直接调用者可能会关心错误并知道如何处理它。因此,您需要设计ParseInt来返回类似的故障代码。

另一方面,如果ParseInt失败,因为它无法分配缓冲区,因为内存非常碎片化,那么调用者不会知道该怎么办。它必须将这个不寻常的错误冒泡到一些处理这些基本故障的层。这对每个人都征税(因为他们必须在自己的API中容纳错误传递机制)。一个例外使得可以跳过这些层(同时仍然确保发生必要的清理)。

当你编写低级代码时,很难决定何时使用传统方法以及何时抛出异常。低级代码必须做出决定(抛出与否)。但它是更高级别的代码,它真正知道什么是预期的,什么是特殊的。

答案 14 :(得分:3)

C ++有几个原因。

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

其次,C ++没有内置垃圾收集,所以拥有资源的C ++类在析构函数中摆脱了它们。因此,在C ++异常处理中,系统必须运行范围内的所有析构函数。在使用GC的语言中,没有真正的构造函数(如Java),抛出异常会减轻负担。

第三,C ++社区,包括Bjarne Stroustrup和标准委员会以及各种编译器编写者,一直认为异常应该是例外。一般来说,反对语言文化是不值得的。这些实现基于异常很少的假设。更好的书籍将异常视为例外。好的源代码使用很少的例外。优秀的C ++开发人员将异常视为例外。与此相反,你需要一个充分的理由,我所看到的所有理由都是为了保持它们的特殊性。

答案 15 :(得分:2)

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

答案 16 :(得分:2)

  1. 可维护性:正如上面提到的那样,在一滴帽子上抛出异常类似于使用gotos。
  2. 互操作性:如果您使用例外,则无法将C ++库与C / Python模块连接(至少不容易)。
  3. 性能下降:RTTI用于实际查找会产生额外开销的异常类型。因此异常不适合处理常见用例(用户输入int而不是字符串等)。

答案 17 :(得分:2)

一个非常实际的原因是,在调试程序时,我经常翻转First Chance Exceptions(Debug - &gt; Exceptions)来调试应用程序。如果发生了很多例外情况,很难找到出现“错误”的地方。

此外,它导致一些反模式,如臭名昭着的“抓住投掷”,并混淆真正的问题。有关详细信息,请参阅我对该主题所做的blog post

答案 18 :(得分:2)

我更喜欢尽可能少地使用例外。异常强制开发人员处理某些可能或可能不是真正错误的情况。有关例外的定义是致命问题还是必须立即处理

的问题。

对此的反驳论点是,只需要懒惰的人输入更多内容就可以自己站起来。

Google的编码政策对never use exceptions说,特别是在C ++中。您的应用程序要么不准备处理异常,要么就是。如果不是,则异常可能会将其传播,直到您的应用程序终止。

找出一些你使用过的库会抛出异常并且你还没有准备好处理它们,这一点都没有用。

答案 19 :(得分: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;
}

第二个例子显然需要一些重构(比如使用设计模式策略),但很好地说明异常不适用于控制流。

例外也有一些性能损失,但性能问题应遵循以下规则:“过早优化是万恶之源”

答案 20 :(得分: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时,开发人员将捕获一个通用异常而不是特定异常,并假设异常是由于他们期望的错误,但可能是由于其他原因。最后,这会导致实例异常被忽略,因为异常成为应用程序流的一部分,而不是例外。

对此的评论可能会增加我自己的答案。

答案 21 :(得分:0)

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

说实话,异常是添加到编程语言中的一个例子,它为开发人员提供了一些正式的要求,可以将处理错误案例的责任从直接的开发人员转移到未来的开发人员。

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

答案 22 :(得分:0)

我认为,“很少使用它”并不是正确的句子。我宁愿“只在特殊情况下投掷”。

许多人已经解释过,为什么在正常情况下不应使用例外。例外有权进行错误处理,纯粹是为了处理错误。

我将重点关注另一点:

另一件事是性能问题。编译器长时间努力让它们变得快速。我不确定,现在的状态如何,但是当你使用控制流的异常时,你会遇到其他麻烦:你的程序会变慢!

原因是,异常不仅是非常强大的goto语句,它们还必须为它们留下的所有帧展开堆栈。因此,隐含地也必须解构堆栈上的对象等等。因此,如果没有意识到这一点,一次异常的抛出将真正涉及到一大堆机制。处理器必须做很多事情。

所以你最终会在不知情的情况下优雅地刻录你的处理器。

所以:仅在特殊情况下使用例外 - 含义:发生真正的错误时!

答案 23 :(得分:0)

我在以下情况下使用例外:

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

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

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

如果无法从本地恢复错误并且应用程序无法继续而没有恢复(您的内存/磁盘空间不足等),则异常是正确的方法。

答案 24 :(得分:0)

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

如果要在调用者忽略编写错误处理代码的情况下提供合理的默认值,或者如果错误最好在调用堆栈中进行处理而不是直接调用者,则应使用IMHO异常。理所当然的默认设置是使用合理的诊断错误消息退出程序。疯狂的替代方案是程序在错误的状态下蹒跚而且崩溃或者在稍后的某个时候静默地产生不良输出,更难以诊断。如果“错误”足够是程序流程的正常部分,调用者无法合理地忘记检查它,则不应使用异常。

答案 25 :(得分:0)

谁说他们应该保守使用?永远不要使用流量控制的异常,就是这样。 那个曾经抛出异常的人是谁呢?

答案 26 :(得分:0)

我的两分钱:

我喜欢使用异常,因为它允许我编程,好像不会发生错误。所以我的代码仍然可读,不会散乱各种错误处理。当然,错误处理(异常处理)被移动到结尾(catch块)或者被认为是调用级别的责任。

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

异常的问题在于它很快变得非常冗长。虽然它意味着允许你的代码保持非常可读,并且只关注正常的事物流,但如果一直使用,几乎每个函数调用都需要包装在try / catch块中,并且它开始失败的目的。

对于前面提到的ParseInt,我喜欢异常的想法。只需返回值。如果参数不可解析,则抛出异常。它一方面使您的代码更清洁。在调用级别,您需要执行类似

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

代码很干净。当我得到像这样分散的ParseInt时,我会创建处理异常的包装函数并返回默认值。 E.g。

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块中,有时代码会变得非常冗长。但有时这仍然有意义。

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

答案 27 :(得分:-1)

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

几乎所有编程规则都是一个准则,说“不要这样做,除非你有一个非常好的理由”:“永远不要使用goto”,“避免全局变量”,“正则表达式预先增加你的数量一个问题“,等等。例外情况也不例外......