断言是异常还是错误?


10

我是一名专业的C程序员,也是一名业余爱好者Obj-C程序员(OS X)。最近,由于语法非常丰富,我很想扩展到C ++。

到目前为止,我对异常的处理还不多。Objective-C有它们,但是苹果公​​司的政策非常严格:

重要说明:您应保留使用编程异常或意外运行时错误(例如,超出范围的集合访问权限,尝试使不可变对象发生突变,发送无效消息以及失去与窗口服务器的连接)的例外情况。

C ++似乎更喜欢使用异常。例如,如果无法打开文件,RAII上的Wikipedia示例将引发异常。Objective-C会return nil因out参数发送错误而导致错误。值得注意的是,似乎std :: ofstream可以设置任何一种方式。

在程序员上我发现了几个答案,要么宣称使用异常代替错误代码,要么根本不使用异常。前者似乎更为普遍。

我还没有发现有人对C ++做过客观的研究。在我看来,由于指针很少使用,因此如果我选择避免异常,则必须使用内部错误标志。它会处理起来太麻烦,还是会比异常情况做得更好?两种情况的比较将是最佳答案。

编辑:虽然不完全相关,但我可能应该澄清一下nil。从技术上讲,它与相同NULL,但事实是,可以向发送消息nil。所以你可以做类似的事情

NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];

[obj retain];

即使第一个电话返回nil。而且,就像您*obj在Obj-C中从未做过的那样,没有空指针取消引用的风险。


恕我直言,如果您向您展示一段Objective C代码如何处理那里的错误会更好。看来,人们在谈论的东西与您所要求的有所不同。
Gangnus

在某种程度上,我想我正在寻找使用异常的理由。因为我对它们的实现方式有所了解,所以我确实知道它们的价格是多少。但是我想如果我能为他们的使用获得足够好的论据,我会去的。
Per Johansson

看来,对于C ++,您需要合理的理由不使用它们。根据这里的反应。
Gangnus 2012年

2
也许吧,但是到目前为止,还没有人解释为什么他们更好(与错误代码相比除外)。我不喜欢在异常情况下使用例外的概念,但是它比基于事实更本能。
Per Johansson,2012年

“我不喜欢...对异常情况使用例外”-同意。
Gangnus 2012年

Answers:


1

C ++似乎更喜欢使用异常。

在某些方面,我建议实际上比Objective-C少,因为C ++标准库通常不会引发程序员错误,例如以最常见的案例设计形式(operator[]即,即)或尝试取消引用无效的迭代器。该语言不会抛出对数组的越界访问,取消引用空指针或任何此类的东西。

实际上,从异常处理方程式中消除程序员的错误实际上消除了其他语言经常通过响应的很大种类的错误throwingassert在这种情况下,C ++倾向于(不会在发布/生产版本中进行编译,而仅在调试版本中进行编译)或只是小故障(经常崩溃),部分原因是该语言不想强加这种运行时检查的成本除非程序员明确希望通过编写自己执行此类检查的代码来支付费用,否则将无法发现此类错误。

Sutter甚至鼓励避免在C ++编码标准中的此类情况下出现异常:

使用异常报告编程错误的主要缺点是,当您希望调试器在检测到违规的确切行上启动且该行的状态完好无损时,您实际上不希望发生堆栈退绕。总结:有些错误可能会发生(请参阅第69到75条)。对于其他所有不应该的事情,如果这样做,则是程序员的错,这是assert

该规则不一定是一成不变的。在某些更关键任务的情况下,最好使用包装器和编码标准,它们统一记录发生程序员错误的位置以及throw存在程序员错误的情况,例如试图对无效的东西进行引用或超出范围,因为如果有机会,在这些情况下无法恢复可能会付出太大的代价。但是总的来说,语言的更普遍使用倾向于倾向于不面对程序员的错误。

外部异常

我看到C ++中最常鼓励异常的地方(例如,根据标准委员会)是“外部异常”的原因,这是程序外部某些外部源的意外结果。一个例子是分配内存失败。另一个原因是无法打开运行该软件所需的关键文件。另一个无法连接到所需的服务器。另一个是用户在没有外部中断的情况下,用户按下中止按钮以取消其普通情况下执行路径希望成功的操作。所有这些都不受即时软件和编写该软件的程序员的控制。它们是来自外部来源的意外结果,这些外部结果阻止了操作(在我的书中应该真正视为不可分割的交易*)成功。

交易次数

我经常鼓励将try区块视为“交易”,因为交易应该整体上成功或整体失败。如果我们尝试执行某项操作,但操作中途失败,则通常需要回退对程序状态所做的任何副作用/更改,以使系统恢复为有效状态,就像从未执行过该事务一样,就像RDBMS无法中途处理查询一样,它也不会损害数据库的完整性。如果您直接在上述事务中更改程序状态,则必须在遇到错误时对其进行“取消更改”(在这里,范围保护对于RAII很有用)。

更简单的选择是不更改原始程序状态。您可以变异它的副本,然后,如果成功,则将其与原始副本交换(确保交换不会抛出)。如果失败,则丢弃副本。即使您通常不将异常用于错误处理,这也适用。如果在遇到错误之前发生了程序状态突变,则“事务性”思维方式是正确恢复的关键。它要么整体成功,要么整体失败。它不能成功地进行突变。

当我看到程序员询问如何正确执行错误或异常处理时,这是最不经常讨论的主题之一,但是在所有想要直接改变程序状态的软件中,这都是最困难的事情。它的运作。纯度和不变性在这里可以帮助实现异常安全,就像它们在线程安全方面的帮助一样,因为不需要回退不会发生的突变/外部副作用。

性能

关于是否使用异常的另一个指导性因素是性能,我并不是用某种强迫性,费力的,适得其反的方式表示。许多C ++编译器都实现了所谓的“零成本异常处理”。

它为零错误执行提供了零运行时开销,甚至超过了C返回值错误处理的开销。作为权衡,异常的传播具有较大的开销。

根据我所读到的内容,它使您的普通情况下执行路径不需要任何开销(甚至不需要通常伴随C样式错误代码处理和传播的开销),以换取将成本大幅度地推向例外路径(这意味着throwing现在比以往任何时候都昂贵)。

“昂贵”有点难以量化,但是,对于初学者来说,您可能不想在某个紧密循环中投入一百万次。这种设计假设异常情况并非一直都在发生。

无错误

而且该性能点使我陷入了无错误的境地,如果我们查看各种其他语言,这将是令人惊讶的模糊。但是我要说的是,鉴于上述的零成本EH设计,您几乎可以肯定不想throw响应在集合中找不到密钥。因为这不仅可以说是非错误的(搜索密钥的人可能已经建立了集合,并期望搜索并不总是存在的密钥),但是在这种情况下,这将是非常昂贵的。

例如,一个集合交集函数可能想要遍历两个集合并搜索它们共有的关键点。如果找不到键threw,则会遍历整个循环,并且可能在一半或更多的迭代中遇到异常:

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

上面的示例绝对是荒谬和夸张的,但是我已经看到,在生产代码中,一些来自其他语言的人在C ++中使用异常,有些像这样,并且我认为这是合理可行的说法,即无论如何都不适用于异常在C ++中。上面的另一个提示是,您会注意到该catch块绝对不做任何事情,而只是为了强行忽略任何此类异常而编写,这通常暗示(尽管不是保证人)在C ++中异常可能没有被适当地使用。

对于那些类型的情况,指示失败的某种返回值类型(从返回false无效的迭代器或nullptr在上下文中有意义的任何东西)通常更合适,并且由于非错误类型的返回值通常也更实用,更具生产力case通常不需要进行一些堆栈展开过程即可到达类比catch站点。

问题

如果我选择避免异常,则必须使用内部错误标志。它会处理起来太麻烦,还是会比异常情况做得更好?两种情况的比较将是最佳答案。

在我看来,完全避免使用C ++产生异常对我产生了反作用,除非您在某些嵌入式系统中工作或禁止使用此类情况(在这种情况下,您还必须全力以赴避免所有情况)库和语言功能,否则throw,例如严格使用nothrow new)。

如果出于某种原因(例如:在您导出的C API的模块的C API边界内工作)绝对必须避免异常,许多人可能与我不同意,但我实际上建议您使用OpenGL等全局错误处理程序/状态glGetError()。您可以使其使用线程本地存储来使每个线程具有唯一的错误状态。

我这样做的理由是,我不习惯看到生产环境中的团队彻底检查所有可能的错误,不幸的是,当返回错误代码时。如果周全,则某些C API几乎可以在每个C API调用中遇到错误,而周全检查将需要以下内容:

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

...几乎所有调用API的代码行都需要进行此类检查。但是我还没有与如此彻底的团队合作的幸运。他们经常有一半甚至有时大部分时间都忽略这种错误。这是我最大的例外。如果我们包装此API并throw在遇到错误时统一使用它,那么异常可能就不会被忽略,并且在我看来,根据经验,这就是异常的优势所在。

但是,如果无法使用异常,那么全局的,每个线程的错误状态至少具有优势(与将错误代码返回给我相比,这是一个巨大的优势),它有可能比迟到的时候稍早捕获以前的错误发生在一些草率的代码库中,而不是完全遗漏它,使我们完全不了解发生了什么。该错误可能在之前的几行或之前的函数调用中发生过,但是只要软件尚未崩溃,我们就可以开始逐步研究并确定发生错误的原因。

在我看来,由于指针很少使用,因此如果我选择避免异常,则必须使用内部错误标志。

我不一定会说指针很少。现在,在C ++ 11及更高版本中甚至有一些方法可以获取容器的基础数据指针和一个新nullptr关键字。如果可以使用类似的方法代替使用原始指针来拥有/管理内存,通常被认为是不明智的,unique_ptr因为在存在异常的情况下遵循RAII的要求至关重要。但是,不拥有/管理内存的原始指针不一定被认为是很糟糕的(即使是来自Sutter和Stroustrup之类的人),有时也非常实用(作为指向事物的索引)。

它们可以说比标准容器迭代器(至少在发行版中,没有检查的迭代器)安全得多,后者不会检测到在无效之后是否尝试取消引用它们。我要说的是,C ++仍然毫无疑问是一种危险的语言,除非您对它的特定使用想要包装所有内容并隐藏甚至不拥有原始指针。几乎至关重要的是,例外情况是资源符合RAII(通常不收取任何运行时费用),但除此之外,它并不一定要成为最安全的语言来避免开发人员不需要的成本。换其他东西。推荐的用法并不是要保护您免受悬空指针和无效的迭代器之类的影响,可以这么说(否则,建议您使用shared_ptr在整个地方,而Stroustrup强烈反对)。它试图防止您在某些情况下无法正确地释放/释放/销毁/解锁/清理资源throws


14

事情就是这样:由于C ++的独特历史和灵活性,您可以找到有人宣称对您想要的任何功能有任何意见。但是,总的来说,您所做的事情看起来像C越多,这个想法就越糟糕。

C ++在异常方面松懈得多的原因之一是,您无法准确地return nil在任何时候都喜欢它。nil绝大多数情况和类型都不存在。

但这是简单的事实。异常会自动执行。当您引发异常时,RAII会接管一切,并且一切由编译器处理。当您使用的错误代码,要记得检查。从本质上讲,这使异常比错误代码要安全得多,它们会像对待错误代码一样进行自我检查。此外,它们更具表现力。引发异常,您可以找出一个字符串来告诉您错误是什么,甚至可以包含特定信息,例如“错误的参数X,其值为Y而不是Z”。收到错误代码0xDEADBEEF,究竟是什么地方出了错?我当然希望文档是完整和最新的,即使您收到“错误的参数”,也永远不会告诉您哪个参数,它具有什么值以及应该具有什么值。如果您也引用引用,它们可能是多态的。最后,可以从错误代码永远不会发生的地方(例如构造函数)抛出异常。通用算法呢?如何std::for_each处理您的错误代码?专家提示:不是。

在各个方面,异常都大大优于错误代码。真正的问题在于异常与断言。

就是这个 没人会事先知道您的程序有哪些运行的前提条件,哪些异常条件是异常的,哪些可以事先检查以及哪些是错误。通常,这意味着您无法在不了解程序逻辑的情况下预先确定给定的故障是断言还是异常。另外,当子操作之一失败时可以继续执行的操作是例外,而不是规则。

我认为,有例外可以捕获。不一定立即,但最终。例外是您希望程序能够从某个点恢复的问题。但是所讨论的操作永远无法从需要例外的问题中恢复。

断言失败始终是致命的,不可恢复的错误。内存损坏之类的事情。

因此,当您无法打开文件时,它是断言还是异常?好吧,在一个通用库中,那么在很多情况下可以处理错误,例如,加载配置文件,您可能只是使用预先构建的默认值,所以我要说例外。

作为一个脚注,我想提到一些“空对象模式”。这种模式是可怕的。十年后,它将是下一个Singleton。可以产生合适的空对象的情况数量很少


替代方法不一定是简单的int。我会更可能使用类似于Obj-C的NSError的东西。
Per Johansson

他不是在与C进行比较,而是与目标C进行了比较。这是一个很大的差异。他的错误代码正在解释行。您说的Al是正确的,只是无法以某种方式回答这个问题。没有冒犯的意思。
Gangnus

任何使用Objective-C的人当然都会告诉您,您绝对是完全错误的。
gnasher729

5

发明异常是有原因的,这是为了避免使所有代码看起来像这样:

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

相反,您得到的内容看起来更像以下内容,其中一个异常处理程序位于其中,main或者位于其他位置:

result1 = function1();
result2 = function2();

有些人声称无例外方法会带来性能优势,但我认为可读性方面的关注超过了性能的微小提高,尤其是当您包括所有if (!success)必须花在所有位置的样板的执行时间时,或者如果您不这样做,则存在难以调试段错误的风险。包括它,并且考虑例外发生的机会相对很少。

我不知道为什么苹果不鼓励使用例外。如果他们试图避免传播未处理的异常,那么您真正要做的就是人们使用空指针来指示异常,因此程序员的错误会导致空指针异常,而不是更有用的文件未找到异常或其他任何东西。


1
如果没有异常的代码看起来像这样,那将更有意义,但通常不是这样。当error_handler永不返回时很少出现这种模式。
Per Johansson,2012年

1

在您引用的帖子中(例外与错误代码),我认为正在进行一个微妙的讨论。问题似乎在于您是否有一些#define错误代码的全局列表,可能包含诸如ERR001_FILE_NOT_WRITEABLE之类的名称(如果不走运,可以缩写)。而且,我认为该线程的主要要点是,如果您使用对象实例使用多态语言进行编程,则不需要这样的过程构造。异常可以仅通过其类型来表示发生了什么错误,并且它们可以封装信息,例如要打印出的消息(以及其他内容)。因此,我将把这种对话视为关于是否应该以面向对象的语言进行程序编程的对话。

但是,关于何时以及在何种程度上依靠异常来处理代码中出现的情况的讨论是另一回事。当引发并捕获异常时,您将引入与传统调用堆栈完全不同的控制流范例。抛出异常基本上是一条goto语句,它使您无法进入调用堆栈,并将您发送到不确定的位置(无论您的客户端代码决定处理异常的位置)。这使得很难对异常逻辑进行推理

因此,有一种像耶列科(Jheriko)所表达的观点,即应将这些情绪降至最低。也就是说,假设您正在读取磁盘上可能存在或不存在的文件。杰里科(Jheriko)和苹果(Apple)以及像他们这样认为的人(包括我本人)会认为抛出异常是不合适的- 可以预期,缺少该文件并非例外。异常不能替代常规控制流程。让您的ReadFile()方法返回一个布尔值也很容易,并且让客户代码从返回值false看到未找到该文件也很容易。然后,客户端代码可以告诉未找到用户文件,或者悄悄地处理这种情况,或者它想要执行的任何操作。

当您引发异常时,就会给您的客户带来负担。您为他们提供的代码有时会使他们脱离正常的调用堆栈,并迫使他们编写其他代码以防止其应用程序崩溃。只有在运行时绝对必要时,或者出于“早期失效”理念的利益,才应将这样强大而繁重的负担强加于他们。因此,在前一种情况下,如果应用程序的目的是监视某些网络运行并且有人拔出网络电缆,则可能引发异常(这是灾难性故障的信号)。在后一种情况下,如果您的方法需要一个非null参数,并且您将被传递为null(表示您使用不当),则可能会引发异常。

至于在面向对象的世界中要做什么而不是例外,除了程序错误代码构造之外,您还有其他选择。我马上想到的是您可以创建一个名为OperationResult的对象或类似的东西。方法可以返回OperationResult,并且该对象可以具有有关该操作是否成功以及您希望其拥有的任何其他信息的信息(例如,状态消息,但更巧妙地,它可以封装错误恢复策略)。返回此结果而不是引发异常可以实现多态,保留控制流,并使调试更加简单。


我同意你的看法,但这是最让我不敢问这个问题的原因。
Per Johansson

0

干净代码中有很多可爱的章节讨论了这个主题-减去了Apple的观点。

基本思想是您永远不会从函数中返回nil,因为它是:

  • 添加很多重复的代码
  • 弄乱了您的代码-即:它变得更难阅读
  • 经常会导致零指针错误-或访问冲突,具体取决于您的特定编程“宗教”

另一个附带的想法是,您使用异常是因为它有助于进一步减少代码中的混乱情况,而不是需要创建一系列错误代码,这些错误代码最终将变得难以跟踪管理和维护(尤其是跨多个模块和平台),并且让客户难以理解,而是创建有意义的消息来标识问题的确切性质,简化调试,并将错误处理代码简化为基本try..catch语句。

在COM开发时代,我有一个项目,其中每个失败都会引发一个异常,并且每个异常都需要使用基于扩展的Windows标准COM异常的唯一错误代码ID。这是先前项目的一个保留项目,该项目以前使用过错误代码,但是该公司希望所有面向对象的项目都可以使用COM流行,而又不会过多地改变它们的处理方式。他们只花了4个月的时间便用完了错误代码号,然后让我重构大量代码以使用异常代替。最好先节省您的精力,并明智地使用异常。


谢谢,但是我不考虑返回NULL。无论如何面对构造函数似乎都不可行。就像我提到的那样,我可能不得不在对象中使用错误标志。
Per Johansson
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.