我的问题是大多数开发人员在错误处理方面更喜欢异常还是错误返回码。请具体说明语言(或语言系)以及为什么您更喜欢其中之一。
我出于好奇问这个问题。个人更喜欢错误返回码,因为它们不会像异常那样爆炸,并且如果用户不想要,也不会强制使用户代码支付异常性能损失。
更新:感谢所有回答!虽然我不喜欢异常的代码流的不可预测性。但有关返回代码(以及其前辈处理程序)的答案确实为代码添加了许多噪音。
我的问题是大多数开发人员在错误处理方面更喜欢异常还是错误返回码。请具体说明语言(或语言系)以及为什么您更喜欢其中之一。
我出于好奇问这个问题。个人更喜欢错误返回码,因为它们不会像异常那样爆炸,并且如果用户不想要,也不会强制使用户代码支付异常性能损失。
更新:感谢所有回答!虽然我不喜欢异常的代码流的不可预测性。但有关返回代码(以及其前辈处理程序)的答案确实为代码添加了许多噪音。
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”的评论)之外,它们很容易被忽略。
使用上述示例,假设有人忘记处理其可能的错误(这种情况会发生...)。当“返回”时,错误被忽略,并且可能在以后引起问题(例如,空指针)。异常则不会出现同样的问题。
错误不会被忽略。但有时,您希望它不会引起问题...因此,您必须仔细选择。
假设我们有以下函数:
如果其中一个调用的函数失败,则doTryToDoSomethingWithAllThisMess的返回类型是什么?
操作符无法返回错误代码。C++构造函数也无法返回错误代码。
上述观点的必然结果。如果我想编写:
CMyType o = add(a, multiply(b, c)) ;
由于返回值已经被使用(有时无法更改),所以我不能这样做。因此,返回值成为第一个参数,并作为引用发送...或者不是。
您可以为每种异常发送不同的类。资源异常(即内存不足)应该很轻,但其他任何异常都可以尽可能重(我喜欢 Java 异常给出整个堆栈)。
然后每个 catch 就可以进行特化。
通常,不应该隐藏错误。如果您不重新抛出,至少在文件中记录错误,在消息框中打开它,或者采取其他措施...
异常的问题在于过度使用它们会产生充满 try/catch 的代码。但问题在于:谁使用 STL 容器来 try/catch 他/她的代码?尽管如此,这些容器仍然可以发送异常。
当然,在 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.
}
(我有一些类似的宏,它们仅在调试状态下激活。)
请注意,在生产环境中,配置文件不存在,因此客户端永远不会看到此宏的结果......但在需要时很容易激活它。
当您使用返回代码编码时,您正在为失败做准备,并希望您的测试堡垒足够安全。
当您使用异常编码时,您知道您的代码可能会失败,并通常在代码中选择的战略位置放置反击捕获。但通常情况下,您的代码更多地涉及“它必须做什么”而不是“我担心会发生什么”。
但是,当您编写代码时,必须使用最好的可用工具,有时最好的方法是“永远不要隐藏错误,并尽快显示错误”。我上面提到的宏遵循这一哲学。
如果(!doSomething())
{
puts("ERROR - doSomething failed");
返回;// 或者对doSomething的失败做出反应
}
如果(!doSomethingElse())
{
// 对doSomethingElse的失败做出反应()
}
- bobobobo我实际上两种都使用。
如果是已知的可能出现错误的情况,我会使用返回代码。如果是我知道会发生的情况,则会发送一个代码。
异常仅用于我没有预料到的事情。
我偏好使用异常来处理C++和Python中的错误。这些语言提供的功能使得抛出、捕获和(必要时)重新抛出异常成为一个明确定义的过程,使得模型易于理解和使用。从概念上讲,使用异常比返回代码更清晰,因为特定的异常可以通过它们的名称进行定义,并且伴随着额外的信息。如果使用返回代码,则仅限于错误值(除非你想定义一个ReturnStatus对象或类似的东西)。
除非您正在编写时间关键性的代码,否则与展开栈帧相关的开销不足以担忧。
对我来说,答案非常明确。当上下文需要尝试或测试者-执行者模式(即CPU密集型或公共API)时,我会额外提供这些方法以实现异常抛出版本。我认为避免异常的普遍规则是误导性的、不被支持的,并且可能导致更多的错误成本,而不是所声称的任何性能问题。
它说如果你正在设计一个 API,请提供帮助该 API 的用户避免抛出异常的方法(尝试和测试者-执行者模式)。
如果可能的话,不要在正常控制流程中使用异常。
除了系统故障和具有潜在竞争条件的操作之外,框架设计人员应该设计 API,以便用户可以编写不会抛出异常的代码。例如,您可以提供一种在调用成员之前检查先决条件的方法,以便用户可以编写不会抛出异常的代码。
推断出的是非测试者/执行者或非 try 实现应在失败时抛出异常,然后用户可以将其更改为您的测试者/执行者或 try 方法以提高性能。成功之坑保持安全,用户选择更危险但性能更佳的方法。
Microsoft 在这里链接1明确表示不要使用返回代码:
❌ 不要返回错误代码。
异常是报告框架中错误的主要方式。
✔️ 通过引发异常来报告执行失败。
并且在这里链接2:
❌ 不要使用错误码,因为担心异常会对性能产生负面影响。
为了提高性能,可以使用 Tester-Doer 模式或 Try-Parse 模式,这两种模式在下面的两个部分中进行了描述。
如果您不使用异常,则可能违反了从非测试器/非尝试实现返回返回代码或布尔值的另一条规则。再次强调,TryParse 不能替代 Parse。它是作为 Parse 的补充提供的。
关于性能:
相对于不抛出异常,异常可能是计算上昂贵的,但它们之所以被称为异常,是有原因的。速度比较总是假定100%的异常率,这应该永远不会发生。即使异常慢100倍,如果只发生1%的时间,那真的有多重要呢?
上下文是一切。例如,为避免唯一键冲突而使用测试-执行或尝试选项往往会浪费更多平均时间和资源(在冲突很少时检查存在性)而不是假设成功输入并捕获这种罕见的冲突。
除非我们谈论图形应用程序等浮点算术,否则相对于开发人员时间,CPU周期是便宜的。
从时间角度看,成本也有同样的论点。相对于数据库查询、Web服务调用或文件加载,正常应用程序时间将使异常时间相形见绌。2006年,异常几乎只需要微秒级。
我敢挑战任何在.NET工作的人,将您的调试器设置为在所有异常上中断,并禁用我的代码,看看有多少异常已经发生,您甚至不知道。
Jon Skeet说"[Exceptions are] not slow enough to make it worth avoiding them in normal use"。链接的回复还包含Jon撰写的两篇文章。他的概括主题是异常很好,如果你将它们作为性能问题体验到,那么可能存在更大的设计问题。
只有在发生意料之外的情况下才应该返回异常。
历史上,异常的另一个重要作用是因为返回代码本质上是专有的。有时从C函数返回0表示成功,有时返回-1或其中任何一个表示失败,而1表示成功。即使它们被枚举,枚举也可能会存在歧义。
异常还可以提供更多信息,特别是明确说明“出了什么问题,这是什么,堆栈跟踪以及一些支持上下文的信息”
话虽如此,良好枚举的返回代码对于已知的结果集可能是有用的,简单地说就是“这个函数有n种结果,并且它只是按照这种方式运行”
设计契约(确保在尝试可能失败的任何操作之前满足前提条件)。这可以捕获大多数问题,并返回错误代码。
在处理工作时返回错误代码(如果需要,则执行回滚)。
异常,但仅用于意外情况。
assert
有同样的问题,即除非始终使用断言运行生产代码,否则无法捕获问题。我倾向于将断言视为在开发过程中捕获问题的一种方式,但仅适用于会始终断言或不断言的内容(例如具有常量的内容,而不是具有变量或任何其他可能在运行时更改的内容)。 - paxdiablo我不喜欢返回码,因为它们会导致以下模式在你的代码中繁衍蔓延。
CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
// bail out
goto FunctionExit;
}
我之前写过一篇关于这个的博客文章。
抛出异常所带来的性能开销不应该影响你的决策。毕竟,如果你做得对,一个异常是异常的。