ChatGPT解决这个技术问题 Extra ChatGPT

哪个,为什么,你更喜欢异常或返回代码?

我的问题是大多数开发人员更喜欢错误处理、异常或错误返回代码。请具体说明语言(或语系)以及为什么您更喜欢其中一种。

我是出于好奇而问这个的。就我个人而言,我更喜欢错误返回代码,因为它们的爆炸性较小,并且如果用户不想这样做,也不会强制用户代码支付异常性能损失。

更新:感谢所有答案!我必须说,尽管我不喜欢带有异常的代码流的不可预测性。关于返回码(和他们的哥哥句柄)的答案确实给代码增加了很多噪音。

软件世界的先有鸡还是先有蛋的问题……永远值得商榷。 :)
对此感到抱歉,但希望有各种各样的意见会帮助人们(包括我自己)做出适当的选择。
通常,对控制流使用异常是一种反模式 softwareengineering.stackexchange.com/questions/189222/…

A
Ayxan Haqverdili

对于某些语言(即 C++),资源泄漏不应成为原因

C++ 基于 RAII。

如果您有可能失败、返回或抛出的代码(即大多数普通代码),那么您应该将指针包装在智能指针中(假设您有充分的理由不在堆栈上创建对象)。

返回码更详细

它们很冗长,并且倾向于发展成以下内容:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

最后,您的代码是标识指令的集合(我在生产代码中看到了这种代码)。

这段代码可以翻译成:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

它干净地将代码和错误处理分开,这可能是一件好事。

返回码更脆弱

如果不是来自某个编译器的一些晦涩的警告(参见 "phjr" 的评论),它们很容易被忽略。

对于上述示例,假设有人忘记处理其可能的错误(发生这种情况......)。该错误在“返回”时会被忽略,并且可能会在以后爆炸(即 NULL 指针)。同样的问题不会发生异常。

错误不会被忽略。有时候,你希望它不爆炸,但是……所以你必须谨慎选择。

有时必须翻译返回码

假设我们有以下功能:

doSomething,它可以返回一个名为 NOT_FOUND_ERROR 的 int

doSomethingElse,它可以返回一个布尔“假”(失败)

doSomethingElseAgain,它可以返回一个错误对象(包含 __LINE__、__FILE__ 和一半的堆栈变量。

doTryToDoSomethingWithAllThisMess 其中,嗯......使用上述函数,并返回类型的错误代码......

如果其调用函数之一失败,则 doTryToDoSomethingWithAllThisMess 的返回类型是什么?

返回码不是通用解决方案

运算符不能返回错误代码。 C++ 构造函数也不能。

返回代码意味着您不能链接表达式

以上观点的推论。如果我想写:

CMyType o = add(a, multiply(b, c)) ;

我不能,因为返回值已被使用(有时,它无法更改)。所以返回值成为第一个参数,作为参考发送......或者不是。

输入异常

您可以为每种异常发送不同的类。资源异常(即内存不足)应该是轻量级的,但其他任何事情都可以根据需要变得很重(我喜欢 Java Exception 给我整个堆栈)。

然后可以对每个捕获进行专门化。

永远不要在不重新抛出的情况下使用 catch(...)

通常,您不应隐藏错误。如果您不重新抛出,至少将错误记录在文件中,打开消息框,无论如何......

例外是...... NUKE

异常的问题是过度使用它们会产生充满尝试/捕获的代码。但问题出在其他地方:谁使用 STL 容器尝试/捕获他/她的代码?尽管如此,这些容器仍然可以发送异常。

当然,在 C++ 中,永远不要让异常退出析构函数。

例外是......同步

确保在它们使您的线程跪下或在您的 Windows 消息循环中传播之前抓住它们。

解决方案可能是混合它们?

所以我想解决方案是在不应该发生的事情时抛出。当某些事情可能发生时,然后使用返回码或参数使用户能够对其做出反应。

所以,唯一的问题是“什么是不应该发生的事情?”

这取决于您的功能合同。如果函数接受一个指针,但指定指针必须为非NULL,那么当用户发送一个NULL指针时抛出异常是可以的(问题是,在C++中,函数作者什么时候不使用引用代替的指针,但是...)

另一种解决方案是显示错误

有时,您的问题是您不希望出现错误。使用异常或错误返回代码很酷,但是……您想了解它。

在我的工作中,我们使用一种“断言”。无论调试/发布编译选项如何,它都会根据配置文件的值:

记录错误

打开一个带有“嘿,你有问题”的消息框

打开一个带有“嘿,你有问题,你想调试”的消息框

在开发和测试中,这使用户能够在检测到问题时准确地查明问题,而不是之后(当某些代码关心返回值时,或者在 catch 内)。

很容易添加到遗留代码中。例如:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

导致一种类似于以下的代码:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(我有类似的宏,仅在调试时有效)。

请注意,在生产中,配置文件不存在,因此客户端永远不会看到此宏的结果...但是在需要时很容易激活它。

结论

当您使用返回码进行编码时,您正在为失败做好准备,并希望您的测试堡垒足够安全。

当您使用异常编码时,您知道您的代码可能会失败,并且通常将 counterfire catch 放在代码中选定的战略位置。但通常,您的代码更多的是关于“它必须做什么”而不是“我担心会发生什么”。

但是当你编写代码时,你必须使用你可以使用的最好的工具,有时,它是“永远不要隐藏错误,并尽快显示它”。我上面所说的宏遵循这个哲学。


是的,但是关于你的第一个例子,可以很容易地写成:if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
为什么不...但是,我仍然发现 doSomething(); doSomethingElse(); ... 更好,因为如果我需要添加 if/while/etc。用于正常执行目的的语句,我不希望它们与 if/while/etc 混合。为特殊目的添加的语句...由于使用异常的真正规则是抛出,而不是捕获,因此 try/catch 语句通常不是侵入性的。
您的第一点显示了异常的问题所在。您的控制流变得很奇怪,并且与实际问题分离。它正在用一连串的捕获量代替某些级别的识别。我会同时使用返回代码(或返回包含大量信息的对象)来处理可能出现的错误和意外情况的异常。
@Peter Weber:这并不奇怪。它与实际问题是分开的,因为它不是正常执行流程的一部分。这是一个特殊的执行。然后,再一次,关于异常的要点是,在发生异常错误的情况下,要经常抛出,并且很少捕获,如果有的话。因此,catch 块甚至很少出现在代码中。
这种辩论中的例子非常简单。通常,“doSomething()”或“doSomethingElse()”实际上会执行某些操作,例如更改某个对象的状态。异常代码不保证将对象返回到之前的状态,甚至当 catch 距离 throw 很远时更不保证......例如,假设 doSomething 被调用了两次,并在抛出之前增加一个计数器。你怎么知道什么时候捕捉到你应该减少一次或两次的异常?一般来说,为任何不是玩具示例的东西编写异常安全代码是非常困难的(不可能?)。
S
Stephen Wrighton

我实际上两者都用。

如果它是已知的、可能的错误,我会使用返回码。如果这是我知道可以并且将会发生的场景,那么就会有一个代码被发回。

例外仅用于我不期望的事情。


+1 非常简单的方法。至少它足够短,以至于我可以很快阅读它。 :-)
“例外仅用于我没有预料到的事情。”如果您不期待它们,那么为什么要使用它们或如何使用它们?
仅仅因为我不期望它会发生,并不意味着我看不到它怎么会发生。我希望我的 SQL Server 能够打开并响应。但是我仍然对我的期望进行编码,以便在发生意外停机时可以优雅地失败。
没有响应的 SQL Server 不会很容易归类为“已知的、可能的错误”吗?
@StephenWrighton 如果您能看到事情是如何发生的,那么您怎么能不期待呢?它是双向的。你似乎做了不同的区分
h
hurst

根据 Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries 中标题为“异常”的第 7 章,给出了许多理由说明为什么对于 C# 等 OO 框架必须使用异常而不是返回值。

也许这是最令人信服的原因(第 179 页):

“异常与面向对象的语言很好地集成。面向对象的语言倾向于对成员签名施加约束,这些约束不是由非 OO 语言中的函数施加的。例如,在构造函数、运算符重载和属性的情况下,开发人员在返回值上没有选择。因此,无法标准化面向对象框架的基于返回值的错误报告。错误报告方法,例如异常,超出方法签名的范围是唯一的选择。”


强烈建议阅读本章。本章提供了一个非常系统的错误/异常处理指南,其中包含许多我在网上找不到的注意事项。
J
Jason Etheridge

我的偏好(在 C++ 和 Python 中)是使用异常。语言提供的工具使其成为一个明确定义的过程,可以引发、捕获和(如有必要)重新抛出异常,从而使模型易于查看和使用。从概念上讲,它比返回代码更简洁,因为可以通过名称定义特定异常,并附带附加信息。使用返回码,您仅限于错误值(除非您想定义 ReturnStatus 对象或其他东西)。

除非您正在编写的代码是时间关键的,否则与展开堆栈相关的开销并不足以担心。


请记住,使用异常会使程序分析更加困难。
j
johnc

只有在您没有预料到的事情发生时才应返回异常。

从历史上看,另一个例外点是返回码本质上是专有的,有时可以从 C 函数返回 0 表示成功,有时返回 -1,或者其中任何一个表示失败,1 表示成功。即使枚举它们,枚举也可能是模棱两可的。

异常还可以提供更多信息,并明确说明“出了点问题,这是什么,堆栈跟踪和上下文的一些支持信息”

话虽这么说,一个很好枚举的返回码对于一组已知的结果很有用,一个简单的“函数的这里有 n 个结果,它只是以这种方式运行”


b
b_levitt

默认情况下始终使用异常,但考虑提供额外的 tester-doer 选项(TryX)!对我来说,答案很明确。当上下文指示 Try 或 Tester-Doer 模式(即 cpu 密集型或公共 api)时,我将另外将这些方法提供给异常抛出版本。我认为避免异常的一揽子规则是错误的、不受支持的,并且可能会导致比他们声称要防止的任何性能问题更多的错误费用。

不,Microsoft 没有说不使用异常(常见的误解)。

它说,如果您正在设计 API,请提供方法来帮助该 API 的用户在需要时避免抛出异常(Try 和 Tester-Doer 模式)

❌ 如果可能,请勿将异常用于正常的控制流程。除了系统故障和具有潜在竞争条件的操作外,框架设计人员应该设计 API,以便用户可以编写不会引发异常的代码。例如,您可以提供一种在调用成员之前检查先决条件的方法,以便用户可以编写不会引发异常的代码。

这里推断的是,非测试执行者/非尝试实现应该在失败时抛出异常,然后用户可以将其更改为您的测试执行者之一或尝试性能方法。成功的坑是为了安全而维护的,用户选择使用更危险但更高效的方法。

Microsoft 确实说过不要使用返回码 TWICE,here

❌ 不要返回错误代码。异常是在框架中报告错误的主要方式。 ✔️ 务必通过抛出异常来报告执行失败。

here

❌ 不要使用错误代码,因为担心异常可能会对性能产生负面影响。为了提高性能,可以使用 Tester-Doer Pattern 或 Try-Parse Pattern,这将在接下来的两节中介绍。

如果您不使用异常,则可能违反了从非测试/非尝试实现返回返回码或布尔值的其他规则。同样,TryParse 不会取代 Parse。它是除了 Parse 之外提供的

主要原因:返回代码几乎每次都未能通过“Pit of Success”测试。

很容易忘记检查返回码,然后稍后会出现红鲱鱼错误。 var 成功 = 保存()?有多少性能值得有人在这里忘记 if 检查? var 成功 = TrySave()?更好,但我们会滥用 TryX 模式吗?您仍然提供 Save 方法吗?

var 成功 = 保存()?有多少性能值得有人在这里忘记 if 检查?

var 成功 = TrySave()?更好,但我们会滥用 TryX 模式吗?您仍然提供 Save 方法吗?

返回代码没有任何关于它们的重要调试信息,如调用堆栈、内部异常。

返回代码不会传播,这与上述几点一起,往往会导致过度和交织的诊断日志记录,而不是记录在一个集中的位置(应用程序和线程级异常处理程序)。

返回码倾向于以嵌套的“if”块的形式驱动混乱的代码

开发人员花费在调试未知问题上的时间,否则这将是一个明显的例外(成功的坑)是昂贵的。

如果 C# 背后的团队不打算用异常来管理控制流,则不会键入异常,catch 语句上将没有“when”过滤器,也不需要无参数的“throw”语句.

关于性能:

相对于根本不抛出异常而言,异常可能在计算上是昂贵的,但出于某种原因,它们被称为异常。速度比较总是设法假设 100% 的异常率,这绝不应该是这种情况。即使一个异常慢了 100 倍,如果它只发生 1% 的时间,这又有什么关系呢?

上下文就是一切。例如,避免唯一键违规的 Tester-Doer 或 Try 选项可能平均浪费更多的时间和资源(在很少发生冲突时检查是否存在),而不是仅仅假设成功进入并捕获罕见的违规。

除非我们在讨论图形应用程序的浮点运算或类似的东西,否则与开发人员时间相比,CPU 周期很便宜。

从时间的角度来看,成本也有同样的论点。相对于数据库查询或 Web 服务调用或文件加载,正常的应用程序时间将使异常时间相形见绌。 2006 年的例外情况接近亚微秒

我敢于在 .net 中工作的任何人设置调试器以中断所有异常并仅禁用我的代码,看看已经发生了多少您甚至不知道的异常。

Jon Skeet 说“[Exceptions are] 不够慢,因此值得在正常使用中避免使用它们”。链接的响应还包含 Jon 关于该主题的两篇文章。他的概括性主题是异常很好,如果您将它们视为性能问题,则可能存在更大的设计问题。


p
paxdiablo

在 Java 中,我使用(按以下顺序):

按合同设计(确保在尝试任何可能失败的事情之前满足先决条件)。这可以捕获大多数东西,我为此返回一个错误代码。在处理工作时返回错误代码(并在需要时执行回滚)。例外,但这些仅用于意想不到的事情。


对合约使用断言不是更正确吗?如果合同被打破,没有什么可以拯救你。
@PawełHajdan,我相信默认情况下禁用断言。这与 C 的 assert 有相同的问题,因为它不会在生产代码中发现问题,除非您一直使用断言运行。我倾向于将断言视为在开发过程中发现问题的一种方法,但适用于将断言或不一致断言的东西(例如带有常量的东西,not带有变量的东西或任何其他可以在运行时改变的东西)。
十二年来回答你的问题。我应该开一个服务台:-)
a
abatishchev

我不喜欢返回码,因为它们会导致以下模式在您的代码中迅速蔓延

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

很快,一个由 4 个函数调用组成的方法调用膨胀到 12 行错误处理。其中一些永远不会发生。 If 和 switch 案例比比皆是。

如果您使用得当,异常会更清晰......发出异常事件的信号......之后执行路径将无法继续。它们通常比错误代码更具描述性和信息性。

如果您在方法调用后有多个状态,应该以不同方式处理(并且不是例外情况),请使用错误代码或输出参数。虽然 Personaly 我发现这很罕见..

我已经对“性能损失”的反驳进行了一些研究。在 C++ / COM 世界中更多的是,但在较新的语言中,我认为差异并不大。在任何情况下,当发生故障时,性能问题都会被放到次要位置:)


T
Thomas

我不久前写了一篇关于此的blog post

抛出异常的性能开销不应在您的决定中发挥任何作用。毕竟,如果你做对了,例外是例外。


链接的博客文章更多地是关于在你跳跃之前查看(检查)与更容易请求宽恕而不是许可(例外或返回码)。我已经回复了我对这个问题的看法(提示:TOCTTOU)。但是这个问题是关于一个不同的问题,即在什么条件下使用语言的异常机制而不是返回具有特殊属性的值。
我完全同意。在过去的九年里,我似乎学到了一两件事;)
S
Smashery

我从 The Pragmatic Programmer 那里得到的一个很好的建议是“你的程序应该能够在不使用异常的情况下执行其所有主要功能”。


你误解了它。他们的意思是“如果你的程序在其正常流程中抛出异常,那就错了”。换句话说,“仅对异常事物使用异常”。
K
Kendall Helmstetter Gelner

我有一套简单的规则:

1) 对您希望直接呼叫者做出反应的事情使用返回码。

2) 对范围更广的错误使用异常,并且可以合理地期望由调用者之上的许多级别来处理,这样错误的意识就不必渗透到许多层,从而使代码更加复杂。

在 Java 中,我只使用过未检查的异常,检查的异常最终只是另一种形式的返回代码,根据我的经验,方法调用可能“返回”的内容的二元性通常更多的是阻碍而不是帮助。


J
Jerub

我在异常和非异常情况下都在 python 中使用异常。

能够使用异常来指示“无法执行请求”通常很好,而不是返回错误值。这意味着您/总是/知道返回值是正确的类型,而不是任意的 None 或 NotFoundSingleton 什么的。这是一个很好的例子,说明我更喜欢使用异常处理程序而不是返回值的条件。

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

副作用是,当运行 datastore.fetch(obj_id) 时,您无需检查其返回值是否为 None,您会立即免费获得该错误。这与“你的程序应该能够在不使用异常的情况下执行其所有主要功能”的论点背道而驰。

这是另一个“异常”有用的异常示例,以便编写代码来处理不受竞争条件影响的文件系统。

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

一个系统调用而不是两个,没有竞争条件。这是一个糟糕的例子,因为很明显,在比目录不存在更多的情况下,这将失败并出现 OSError,但对于许多严格控制的情况来说,它是一个“足够好”的解决方案。


第二个例子具有误导性。据说错误的方式是错误的,因为 os.path.rmdir 代码旨在引发异常。返回代码流中的正确实现是 'if rmdir(...)==FAILED: pass'
r
rpattabi

我相信返回码会增加代码噪音。例如,由于返回码,我一直讨厌 COM/ATL 代码的外观。必须对每一行代码进行 HRESULT 检查。我认为错误返回代码是 COM 架构师做出的错误决定之一。这使得对代码进行逻辑分组变得困难,因此代码审查变得困难。

当每行都明确检查返回码时,我不确定性能比较。


COM wad 旨在供不支持异常的语言使用。
这是一个好点。处理脚本语言的错误代码是有意义的。至少 VB6 很好地隐藏了错误代码细节,并将它们封装在 Err 对象中,这在一定程度上有助于更清晰的代码。
我不同意:VB6 只记录最后一个错误。结合臭名昭著的“on error resume next”,当您看到问题时,您将完全错过问题的根源。请注意,这是 Win32 APII 中错误处理的基础(参见 GetLastError 函数)
s
superjos

异常不适用于错误处理,IMO。例外就是这样;您没有预料到的异常事件。我说谨慎使用。

错误代码可以,但从方法返回 404 或 200 是不好的,IMO。改用枚举(.Net),这使得代码更易读,更容易被其他开发人员使用。此外,您不必维护一个包含数字和描述的表格。

还; try-catch-finally 模式在我的书中是一个反模式。 try-finally 可以很好,try-catch 也可以很好,但是 try-catch-finally 永远不会很好。 try-finally 通常可以用“使用”语句(IDispose 模式)代替,这是更好的 IMO。并且 Try-catch 在您实际捕获您能够处理的异常的地方是好的,或者如果您这样做:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

因此,只要您让异常继续冒泡就可以了。另一个例子是这样的:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

在这里,我实际上处理了异常;当更新方法失败时,我在 try-catch 之外所做的是另一回事,但我认为我的观点已经提出。 :)

那么为什么 try-catch-finally 是一种反模式呢?原因如下:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

如果 db 对象已经关闭,会发生什么?一个新的异常被抛出,它必须被处理!这个更好:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

或者,如果 db 对象没有实现 IDisposable,请执行以下操作:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

无论如何,那是我的 2 美分! :)


如果一个对象有 .Close() 但没有 .Dispose() 会很奇怪
我只是以 Close() 为例。随意将其视为其他内容。正如我所说;如果可用,应该使用 using 模式(doh!)。这当然意味着该类实现了 IDisposable,因此您可以调用 Dispose。
N
Nathan Long

我通常更喜欢返回码,因为它们让调用者决定失败是否异常。

这种方法在 Elixir 语言中很典型。

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

人们提到返回码会导致您有很多嵌套的 if 语句,但可以使用更好的语法来处理。在 Elixir 中,with 语句让我们可以轻松地将一系列快乐路径返回值与任何失败区分开来。

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir 仍然具有引发异常的函数。回到我的第一个示例,如果无法写入文件,我可以执行其中任何一项来引发异常。

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

如果我作为调用者知道我想在写入失败时引发错误,我可以选择调用 File.write! 而不是 File.write。或者我可以选择调用 File.write 并以不同的方式处理每个可能的失败原因。

当然,如果我们愿意,rescue 异常总是可能的。但与处理信息丰富的返回值相比,这对我来说似乎很尴尬。如果我知道函数调用可能会失败甚至应该失败,那么它的失败并不是例外情况。


K
Kyle Cronin

对于任何体面的编译器或运行时环境,异常不会招致重大损失。它或多或少有点像跳转到异常处理程序的 GOTO 语句。此外,让运行时环境(如 JVM)捕获异常有助于更轻松地隔离和修复错误。任何一天,我都会在 Java 中使用 NullPointerException 来处理 C 中的段错误。


例外是非常昂贵的。他们必须遍历堆栈以找到潜在的异常处理程序。这种堆栈步行并不便宜。如果构建堆栈跟踪,则成本更高,因为必须解析整个堆栈。
我很惊讶编译器至少在某些时候无法确定异常会在哪里被捕获。另外,在我看来,异常会改变代码流这一事实可以更容易地准确识别错误发生的位置,从而弥补了性能损失。
调用堆栈在运行时会变得非常复杂,编译器通常不会进行这种分析。即使他们这样做了,您仍然必须遍历堆栈才能找到踪迹。您还必须展开堆栈以处理堆栈分配对象的 finally 块和析构函数。
不过,我确实同意异常的调试好处通常可以弥补性能成本。
Derak Park,异常发生时代价高昂。这就是不应过度使用它们的原因。但是当它们不发生时,它们几乎没有成本。
T
Trent

我更喜欢将异常用于错误处理并将返回值(或参数)作为函数的正常结果。这提供了一个简单且一致的错误处理方案,如果正确完成,它会使代码看起来更清晰。


D
Daniel Bruce

最大的区别之一是异常迫使您处理错误,而错误返回代码可以不检查。

如果大量使用错误返回代码,也会导致非常丑陋的代码,其中包含大量类似于此形式的 if 测试:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

就我个人而言,我更喜欢对调用代码应该或必须处理的错误使用异常,并且只将错误代码用于“预期的失败”,其中返回的东西实际上是有效且可能的。


至少在 C/C++ 和 gcc 中,你可以给一个函数一个属性,当它的返回值被忽略时会生成一个警告。
phjr:虽然我不同意“返回错误代码”模式,但您的评论也许应该成为一个完整的答案。我觉得足够有趣。至少,它确实给了我一个有用的信息。
A
Adam Bellaire

有很多理由更喜欢异常而不是返回码:

通常,为了便于阅读,人们会尽量减少方法中返回语句的数量。这样做,异常可以防止在不正确状态下做一些额外的工作,从而防止潜在地损坏更多数据。

异常通常比返回值更详细,更易于扩展。假设一个方法返回自然数并且当错误发生时你使用负数作为返回码,如果你的方法的范围改变并且现在返回整数,你将不得不修改所有的方法调用而不是仅仅调整一点点例外。

异常允许更容易地分离正常行为的错误处理。它们允许确保某些操作以某种方式作为原子操作执行。


S
SCdF

我只使用异常,没有返回码。我在这里谈论Java。

我遵循的一般规则是,如果我有一个名为 doFoo() 的方法,那么如果它没有“做 foo”,那么就发生了一些异常情况,应该抛出一个异常。


R
Robert Gould

我担心异常的一件事是抛出异常会破坏代码流。例如,如果你这样做

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

或者更糟糕的是,如果我删除了一些我不应该删除的东西,但在我完成其余的清理工作之前就被扔掉了。投掷给可怜的用户恕我直言。


这就是 finally 语句的用途。但是,唉,它不在 C++ 标准中......
在 C++ 中,您应该遵循经验法则“在构造函数中获取资源并在析构函数中释放它们。对于这种特殊情况,auto_ptr 将完美地完成。
托马斯,你错了。 C++ 没有 finally 因为它不需要它。它有 RAII。 Serge 的解决方案是使用 RAII 的一种解决方案。
罗伯特,使用 Serge 的解决方案,你会发现你的问题消失了。现在,如果你写的 try/catch 比 throws 多,那么(通过判断你的评论)你的代码可能有问题。当然,使用 catch(...) 而不重新抛出通常是不好的,因为它隐藏了错误以更好地忽略它。
J
Jon Limjap

我在异常与返回代码参数中的一般规则:

当您需要本地化/国际化时使用错误代码——在 .NET 中,您可以使用这些错误代码来引用资源文件,然后该文件将以适当的语言显示错误。否则,使用异常

仅对真正异常的错误使用异常。如果这是经常发生的事情,请使用布尔值或枚举错误代码。


当您执行 l10n/i18n 时,没有理由不能使用异常。异常也可以包含本地化信息。
J
Jonathan Adelson

我没有发现返回码比异常更难看。除此以外,您拥有 try{} catch() {} finally {},而返回代码则拥有 if(){}。由于帖子中给出的原因,我曾经担心例外;你不知道指针是否需要清除,你有什么。但我认为在返回码方面你也有同样的问题。除非您知道有关函数/方法的一些详细信息,否则您不知道参数的状态。

无论如何,如果可能,您必须处理错误。您可以很容易地让异常传播到顶层,就像忽略返回码并让程序出现段错误一样。

我确实喜欢为结果返回一个值(枚举?)并为异常情况返回一个异常的想法。


2
2ank3th

对于像 Java 这样的语言,我会选择 Exception,因为如果不处理异常,编译器会给出编译时错误。这会强制调用函数处理/抛出异常。

对于Python,我比较矛盾。没有编译器,因此调用者可能没有处理导致运行时异常的函数抛出的异常。如果您使用返回码,如果处理不当,您可能会出现意外行为,如果您使用异常,您可能会遇到运行时异常。


C
Carsten Führmann

到目前为止,在这个非常有趣的讨论中还有一些重要的方面没有被提及。

首先,重要的是要注意异常不适用于分布式计算,但错误代码仍然适用。想象一下分布在多个服务器上的通信服务。一些通信甚至可能是异步的。这些服务甚至可能使用不同的技术堆栈。显然,错误处理概念在这里至关重要。显然,在这种最一般的情况下不能使用异常,因为错误必须是“通过电缆”发送的序列化内容,甚至可能以与语言无关的方式发送。从这个角度来看,错误代码(实际上是错误消息)比异常更普遍。一旦假设系统架构师的观点并且事情需要扩展,就需要良好的错误消息功夫。

第二点非常不同,它是关于一种语言是否或如何代表受歧视的工会。这个问题严格来说是关于“错误代码”的。一些答案也是如此,提到错误代码不能像异常一样很好地传输信息。如果错误代码是数字,则为 true。但是为了与异常进行更公平的对比,应该考虑区分联合类型的错误值。因此,被调用者的返回值将是可区分的联合类型,它可能是所需的快乐路径值或异常将具有的有效负载。这种方法多久优雅到可取取决于编程语言。例如,F# 具有超级优雅的可区分联合和相应的模式匹配。在这样的语言中,避免异常会比在 C++ 中更有吸引力。

第三点也是最后一点是关于函数式编程和纯函数的。例外是(以实际的方式和以理论-计算机-科学的方式)“副作用”。换句话说,处理异常的函数或方法不是纯粹的。 (一个实际的结果是,除了例外,必须注意评估顺序。)相比之下,错误值是纯的,因为它们只是普通的返回值,不涉及任何副作用。因此,函数式程序员可能比面向对象的程序员更可能对异常不屑一顾。 (特别是如果该语言还具有上述区分联合的优雅表示。)