我的问题是,大多数开发人员更喜欢错误处理,异常或错误返回码。请特定于语言(或语言家族),以及为什么您偏爱另一种语言。
我出于好奇而问这个。就我个人而言,我更喜欢错误返回码,因为它们的爆炸性较小,并且如果不想这样做,也不必强迫用户代码支付异常性能损失。
更新:感谢您的所有答案!我必须说,尽管我不喜欢带有异常的代码流的不可预测性。关于返回码(及其兄长句柄)的答案确实给代码增加了很多噪音。
Answers:
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指针)。异常不会发生相同的问题。
该错误将不会被忽略。有时候,您希望它不会爆炸……因此,您必须谨慎选择。
假设我们具有以下功能:
如果doTryToDoSomethingWithAllThisMess的返回函数之一失败,返回的类型是什么?
操作员无法返回错误代码。C ++构造函数也不能。
以上几点的推论。如果我想写怎么办:
CMyType o = add(a, multiply(b, c)) ;
我不能,因为返回值已经被使用了(有时不能更改)。因此,返回值成为第一个参数,作为参考发送。
您可以为每种异常发送不同的类。资源异常(即内存不足)应该比较轻,但是其他任何东西都可以根据需要进行繁重的处理(我喜欢Java Exception,它为我提供了整个堆栈)。
每个渔获物然后可以是专门的。
通常,您不应该隐藏错误。如果至少不重新抛出,则将错误记录在文件中,打开一个消息框,无论如何...
例外的问题是过度使用它们会产生充满try / catches的代码。但是问题出在别处:谁使用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.
}
(我有类似的宏,它们仅在调试时才处于活动状态)。
请注意,在生产环境中,配置文件不存在,因此客户端永远不会看到此宏的结果...但是在需要时很容易将其激活。
当使用返回码进行编码时,您正在为失败做准备,并希望您的测试堡垒足够安全。
当您使用异常进行编码时,您知道您的代码可能会失败,并且通常会在代码中选定的战略位置放上反扑之门。但是通常,您的代码更多地是关于“它必须做什么”,而不是“我担心会发生什么”。
但是,当您编写代码时,必须使用最好的工具,有时,这是“永远不要隐藏错误,并尽快显示它”。我上面所说的宏遵循这种哲学。
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混合使用。出于特殊目的而添加的语句...由于使用异常的真正规则是throw而不是catch,try / catch语句通常不是侵入性的。
我实际上都使用。
如果有已知的可能错误,我将使用返回码。如果我知道这种情况可以并且将会发生,那么有一个代码会被发回。
异常仅用于我不期望的事情。
我的偏好(在C ++和Python中)是使用异常。语言提供的功能使其成为引发,捕获和(如有必要)重新抛出异常的明确定义的过程,从而使模型易于查看和使用。从概念上讲,它比返回码干净,因为可以通过异常的名称定义特定的异常,并附带其他信息。使用返回码,您将仅限于错误值(除非要定义ReturnStatus对象或其他内容)。
除非您正在编写的代码对时间要求严格,否则与展开堆栈相关的开销并不值得担心。
在Java中,我按以下顺序使用:
按合同设计(在尝试任何可能失败的项目之前,请确保满足前提条件)。这捕获了大多数东西,为此我返回了一个错误代码。
在处理工作时返回错误代码(并在需要时执行回滚)。
异常,但仅用于意外情况。
assert
东西有同样的问题,除非您一直运行断言,否则它将不会在生产代码中发现问题。我倾向于将断言视为在开发过程中发现问题的一种方式,但仅适用于将断言或未断言一致的东西(例如带有常量的东西,不带有变量的东西或在运行时可能发生变化的其他东西)。
返回码失败“成功的坑几乎每一次”考验我。
关于性能:
不久前,我写了一篇博客文章。
引发异常的性能开销在您的决策中不起作用。毕竟,如果您做得对,那就是例外。
我不喜欢返回代码,因为它们会导致以下模式在整个代码中迅速蔓延
CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
// bail out
goto FunctionExit;
}
不久,由4个函数调用组成的方法调用会膨胀并带有12行错误处理。.其中一些永远不会发生。如果和切换情况比比皆是。
如果使用得当,异常会更干净...发出异常事件的信号..之后执行路径将无法继续。它们通常比错误代码更具描述性和信息性。
如果在方法调用后有多个状态,应以不同的方式处理(并非例外),请使用错误代码或out参数。尽管Personaly我发现这种情况很少见。
在C ++ / COM世界中,我对“性能下降”的反驳颇有争议。但是在较新的语言中,我认为差别不大。在任何情况下,当发生问题时,性能问题都将归咎于落后者:)
对于任何不错的编译器或运行时环境,异常不会造成重大损失。它或多或少类似于跳转到异常处理程序的GOTO语句。此外,运行时环境(如JVM)捕获异常有助于更轻松地隔离和修复错误。我每天都会通过C中的段错误来获取Java中的NullPointerException。
finally
堆栈分配对象的块和析构函数。
我有一组简单的规则:
1)使用返回码处理您希望直接呼叫者做出反应的事情。
2)对范围更广的错误使用异常,并且可以合理地预期由调用者之上许多级别的东西来处理错误,从而使错误的意识不必遍及许多层,从而使代码更加复杂。
在Java中,我只使用过非检查型异常,检查型异常最终只是返回代码的另一种形式,根据我的经验,方法调用可能“返回”的二重性通常是障碍而不是帮助。
在异常和非异常情况下,我都在python中使用Exceptions。
与返回Error值相反,能够使用Exception来指示“无法执行请求”通常是很好的选择。这意味着您/总是/知道返回值是正确的类型,而不是任意的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失败,但是对于许多严格控制的情况,这是一个“足够好”的解决方案。
我相信返回码会增加代码噪音。例如,由于返回代码,我一直讨厌COM / ATL代码的外观。必须对每行代码进行HRESULT检查。我认为错误返回码是COM架构师做出的错误决定之一。这使得很难对代码进行逻辑分组,因此代码审查变得困难。
当对每行的返回代码进行显式检查时,我不确定性能比较。
最大的区别之一是,异常会强制您处理错误,而错误返回码可能会未经检查。
错误返回码如果大量使用,也可能导致非常丑陋的代码,其中包含许多类似于以下形式的if测试:
if(function(call) != ERROR_CODE) {
do_right_thing();
}
else {
handle_error();
}
就我个人而言,我更喜欢使用异常来处理调用代码应该或必须执行的错误,并且仅将错误代码用于“预期的失败”,以返回实际有效且可能的返回值。
IMO不针对错误处理提供例外。例外就是这样。您没想到的特殊事件。我说请谨慎使用。
错误代码可以,但是从方法中返回404或200不好,IMO。使用枚举(.Net)代替,这会使代码更具可读性,并易于其他开发人员使用。另外,您不必维护有关数字和描述的表格。
也; try-catch-finally模式是我书中的反模式。try-finally可能很好,try-catch也可能很好,但是try-catch-finally永远不好。try-finally通常可以用“ using”语句(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;
}
在这里,我实际上处理了异常;当update方法失败时,我在try-catch之外所做的是另一回事,但我认为我的观点已经提出。:)
为什么最后尝试赶上反模式?原因如下:
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美分!:)
我担心异常的一件事是,抛出异常会破坏代码流。例如,如果您这样做
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?
}
}
甚至更糟的是,如果我删除了本不应该拥有的内容,但是在进行其余的清理工作之前就被赶上了。投掷给可怜的用户恕我直言很大的负担。
我通常更喜欢返回码,因为它们可以让调用者确定失败是否是异常的。
这种方法在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
如果我们愿意,总是有可能例外。但是与处理信息丰富的返回值相比,对我来说似乎很尴尬。如果我知道一个函数调用可能失败,甚至应该失败,那么它的失败也不例外。