我们更喜欢哪种和为什么使用例外或者返回码?

时间:2020-03-06 14:24:43  来源:igfitidea点击:

我的问题是,大多数开发人员更喜欢错误处理,异常或者错误返回码。请特定于语言(或者语言家族),以及为什么我们偏爱另一种语言。

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

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

解决方案

我实际上都使用。

如果这是已知的可能的错误,我将使用返回码。如果我知道这种情况可以并且将会发生,那么有一个代码会被发送回去。

异常仅用于我不期望的事情。

对于任何合适的编译器或者运行时环境,异常不会造成重大损失。它或者多或者少类似于跳转到异常处理程序的GOTO语句。此外,运行时环境(如JVM)捕获异常有助于更轻松地隔离和修复错误。我每天都会通过C中的段错误在Java中获取NullPointerException。

我的偏好(在C ++和Python中)是使用异常。语言提供的功能使其成为引发,捕获和(如有必要)重新抛出异常的明确定义的过程,从而使模型易于查看和使用。从概念上讲,它比返回码干净,因为可以通过异常的名称定义特定的异常,并附带其他信息。使用返回码,我们将仅限于错误值(除非要定义ReturnStatus对象或者其他内容)。

除非我们正在编写的代码对时间要求严格,否则与展开堆栈相关的开销并不值得担心。

仅在发生意外情况时才返回异常。

从历史上看,异常的另一点是返回码具有固有的专有性,有时从C函数返回0表示成功,有时返回-1,或者其中之一表示失败,而1表示成功。即使枚举,枚举也可能是模棱两可的。

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

话虽这么说,一个良好枚举的返回码对于已知的一组结果很有用,一个简单的"函数的n个结果,就这样运行"

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

我遵循的一般规则是,如果我有一个名为doFoo()的方法,则它遵循的是,如果它不像以前那样" do foo",则发生了异常情况,应抛出异常。

我从The Pragmatic Programmer获得了一个很好的建议,即"程序应该能够在不使用任何例外的情况下执行其所有主要功能"。

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

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?
  }
}

甚至更糟的是,如果我删除了本不应该删除的内容,但是在进行其余的清理工作之前被赶上了。投掷给可怜的用户恕我直言带来了很大的压力。

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

最大的区别之一是,异常会强制我们处理错误,而错误返回码可能会未经检查。

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

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

就我个人而言,我更喜欢使用异常来处理调用代码应该或者必须执行的错误,并且仅将错误代码用于"预期的失败",以返回实际有效且可能的东西。

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

  • 按合同设计(在尝试任何可能失败的项目之前,请确保满足前提条件)。这捕获了大多数东西,为此我返回了一个错误代码。
  • 在处理工作时返回错误代码(并在需要时执行回滚)。
  • 异常,但仅用于意外情况。

我在例外与返回码参数中的一般规则:

  • 需要本地化/国际化时使用错误代码-在.NET中,我们可以使用这些错误代码来引用资源文件,该文件随后将以适当的语言显示错误。否则,请使用例外
  • 仅将异常用于真正异常的错误。如果这种情况经常发生,请使用布尔值或者枚举错误代码。

不久前,我写了一篇有关此的博客文章。

引发异常的性能开销不应在决定中发挥任何作用。毕竟,如果我们做得对,那就是例外。

有很多原因比返回代码更喜欢异常:

  • 通常,出于可读性考虑,人们尝试将方法中return语句的数量减至最少。这样做,异常会阻止处于不正确状态时进行一些额外的工作,从而防止可能损坏更多数据。
  • 通常,异常比返回值更冗长,而且更容易扩展。假定方法返回自然数,并且在发生错误时使用负数作为返回码,如果方法的范围发生变化并且现在返回整数,则我们将必须修改所有方法调用,而不仅仅是进行一些调整例外。
  • 异常允许更轻松地将正常行为的错误处理分开。它们允许确保某些操作以某种方式执行为原子操作。

我有一组简单的规则:

1)将返回码用于我们希望直接呼叫者做出反应的事情。

2)对范围更广的错误使用异常,并且可以合理地预期由调用者之上许多级别的东西来处理错误,从而使错误的意识不必遍及许多层,从而使代码更加复杂。

在Java中,我只使用过非检查异常,检查异常最终只是返回代码的另一种形式,根据我的经验,方法调用可能"返回"的二重性通常是障碍,而不是帮助。

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

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

很快,由4个函数调用组成的方法调用会膨胀并带有12行错误处理。.其中一些永远不会发生。如果和切换情况比比皆是。

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

如果在方法调用后有多个状态,应以不同的方式处理(并非例外),请使用错误代码或者out参数。尽管Personaly我发现这种情况很少见。

在C ++ / COM世界中,我对"性能下降"的反驳颇有争议。但是在较新的语言中,我认为差别不大。无论如何,当发生问题时,性能方面的顾虑都将归咎于后援:)

我发现返回码并不比异常丑陋。除此以外,我们有try {} catch(){}最终{},其中与返回码一样,我们还有if(){}。由于帖子中给出的原因,我过去常常害怕例外。我们不知道是否需要清除指针,我们呢?但是我认为我们在返回码方面也有同样的问题。我们不知道参数的状态,除非我们知道有关所讨论的函数/方法的一些详细信息。

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

我很喜欢为结果返回值(枚举?)和为特殊情况返回异常的想法。

在异常和非异常情况下,我都在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架构师做出的错误决定之一。这使得很难对代码进行逻辑分组,因此代码审查变得困难。

当对每行的返回代码进行显式检查时,我不确定性能比较。

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美分! :)

根据《框架设计指南:可重用.NET库的约定,惯用语和模式》中标题为"异常"的第7章,给出了许多理由说明为什么对于C#这样的OO框架必须在返回值上使用异常。

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

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

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,它可以返回布尔值" false"(表示失败)
  • doSomethingElseSagain,它可以返回一个Error对象(具有__LINE FILE__和一半堆栈变量。
  • doTryToDoSomethingWithAllThisMess其中,使用上面的函数,并返回类型为错误的错误代码...

如果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.
}

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

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

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

当我们使用异常进行编码时,我们知道代码可能会失败,并且通常会将反火赶到代码中选定的战略位置。但是通常,代码更多是关于"它必须做什么",而不是"我担心会发生什么"。

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