关于语言不可知:您更喜欢例外或返回代码的原因和原因?

关于语言不可知:您更喜欢例外或返回代码的原因和原因?

Which, and why, do you prefer Exceptions or Return codes?

我的问题是大多数开发人员更喜欢错误处理,异常或错误返回代码。 请具体说明语言(或语言家族)以及您喜欢哪一种。

我出于好奇而问这个问题。 我个人更喜欢错误返回代码,因为它们不那么具有爆炸性,并且如果不想要,也不会强制用户代码支付异常性能损失。

更新:感谢所有答案! 我必须说,虽然我不喜欢代码流与异常的不可预测性。 关于返回代码(以及他们的哥哥句柄)的答案会给代码添加大量的噪音。


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

C ++基于RAII。

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

返回代码更详细

它们很冗长,并且倾向于发展成类似的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
}

最后,您的代码是一组精心指令(我在生产代码中看到了这种代码)。

这段代码可以很好地翻译成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,可以返回bool"false"(失败)
  • doSomethingElseSagain,它可以返回一个Error对象(包含__LINE __,__ FILE__和一半堆栈变量。
  • doTryToDoSomethingWithAllThisMess,嗯......使用上面的函数,并返回类型错误代码...
  • 如果其中一个被调用的函数失败,doTryToDoSomethingWithAllThisMess的返回类型是什么?

    返回代码不是通用的解决方案

    操作员无法返回错误代码。 C ++构造函数也不能。

    返回代码表示您无法链接表达式

    上述观点的必然结果。如果我想写怎么办:

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

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

    键入例外

    您可以为每种异常发送不同的类。 Ressources异常(即内存不足)应该很轻,但其他任何东西都可能是必要的(我喜欢Java异常给我整个堆栈)。

    然后每个捕获物都可以专门化。

    不要在没有重新投掷的情况下使用catch(...)

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

    例外是...... NUKE

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

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

    例外是...同步

    一定要抓住它们,然后再将它们放在膝盖上,或者在Windows消息循环中传播。

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

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

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

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

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

    有时,您的问题是您不想要错误。使用异常或错误返回代码很酷,但是......你想知道它。

    在我的工作中,我们使用了一种"断言"。无论调试/发布编译选项如何,它都将取决于配置文件的值:

  • 记录错误
  • 用"嘿,你有问题"打开一个消息框
  • 用"嘿,你有问题,你想调试"打开一个消息框
  • 在开发和测试中,这使用户能够在检测到问题时精确查明问题,而不是在(某些代码关心返回值或陷阱内)之后。

    可以轻松添加到旧代码中。例如:

    1
    2
    3
    4
    void doSomething(CMyObject * p, int iRandomData)
    {
       // etc.
    }

    引出一种类似于以下的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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 !
    iRandomData is equal to" << iRandomData <<". Will throw.") ;
          throw std::some_exception() ;
       }

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

       // etc.
    }

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

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

    结论

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

    当您使用异常进行编码时,您知道您的代码可能会失败,并且通常会将反火箭捕获到代码中选定的战略位置。但通常,你的代码更多的是"它必须做什么"然后"我担心会发生什么"。

    但是当您编写代码时,您必须使用最好的工具,有时候,它是"永远不会隐藏错误,并尽快显示"。我上面谈到的宏观遵循这一理念。

    好。


    我实际上都用了。

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

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


    根据框架设计指南中的第7章"例外":可重用.NET库的约定,惯用语和模式,给出了为什么对于诸如C#的OO框架使用返回值的异常的原因。

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

    "异常与面向对象语言很好地集成。面向对象语言倾向于对非OO语言中的函数强加的成员签名施加约束。例如,在构造函数,运算符重载和属性的情况下,开发人员在返回值中没有选择。因此,无法对面向对象框架的基于返回值的错误报告进行标准化。一种错误报告方法,例如异常,它是方法签名的带外是唯一的选择。"


    我的偏好(在C ++和Python中)是使用异常。语言提供的工具使其成为一个定义良好的过程,既可以提升,捕获和(如有必要)重新抛出异常,使模型易于查看和使用。从概念上讲,它比返回代码更清晰,因为特定的例外可以通过其名称来定义,并附带其他信息。使用返回代码,您仅限于错误值(除非您要定义ReturnStatus对象或其他内容)。

    除非您编写的代码对时间要求严格,否则与展开堆栈相关的开销并不足以让人担心。


    只有在您不期望发生的事情发生时,才能返回例外情况。

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

    例外也可以提供更多的信息,特别是说明"错误的东西,这里是什么,堆栈跟踪和上下文的一些支持信息"

    话虽这么说,一个列举良好的返回代码对于一组已知的结果非常有用,这是一个简单的"函数结果,它只是以这种方式运行"


    我刚才写了一篇关于这个的博客文章。

    抛出异常的性能开销不应该在您的决策中起任何作用。毕竟,如果你做得对,一个例外也是例外。


    回复代码几乎每次都没有通过"成功之坑"测试。

    • 忘记检查返回码然后稍后出现红鲱鱼错误太容易了。
    • 返回代码没有任何关于它们的很好的调试信息,如调用堆栈,内部异常。
    • 返回代码不会传播,与上述点一起,往往会驱动过多和交织的诊断日志记录,而不是登录到一个集中的位置(应用程序和线程级异常处理程序)。
    • 返回代码往往会以嵌套的"if"块的形式驱动混乱的代码
    • 开发人员花在调试未知问题上的时间本来是一个明显的例外(成功的陷阱)是昂贵的。
    • 如果C#背后的团队不打算用异常来控制控制流,那么就不会输入异常,catch语句上也没有"when"过滤器,也就没有必要使用无参数的'throw'语句。

    关于表现:

    • 例外可能是计算上昂贵的相对于完全不投掷,但它们被称为EXCEPTIONS是有原因的。速度比较总是设法假设100%的异常率,而这种情况绝对不是这种情况。即使异常速度慢100倍,如果它只发生1%的时间,这真的很重要吗?
    • 除非我们讨论图形应用程序或类似的浮点运算,否则CPU周期与开发人员时间相比便宜。
    • 从时间角度来看,成本具有相同的论点。相对于数据库查询或Web服务调用或文件加载,正常的应用程序时间将使异常时间相形见绌。 2006年的例外情况几乎是次MICRO秒

      • 我敢于在.net中工作,设置你的调试器来打破所有异常,只禁用我的代码,看看有多少异常已经发生,你甚至都不知道。

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

    1
    2
    3
    4
    5
    6
    7
    CRetType obReturn = CODE_SUCCESS;
    obReturn = CallMyFunctionWhichReturnsCodes();
    if (obReturn == CODE_BLOW_UP)
    {
      // bail out
      goto FunctionExit;
    }

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

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

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

    我已经在关于'性能惩罚'的反驳中找到了一点...更多的是在C ++ / COM世界中,但在较新的语言中,我认为差别并不大。在任何情况下,当事情爆发时,性能问题将降级为后备者:)


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

  • 按合同设计(在尝试可能失败的任何事情之前确保满足先决条件)。这捕获了大多数东西,我为此返回错误代码。

  • 处理工作时返回错误代码(如果需要,执行回滚)。

  • 例外,但这些只用于意外事情。


  • IMO不支持错误处理。例外就是这样;你没想到的特殊事件。请谨慎使用我说。

    错误代码可以正常,但从方法返回404或200是不好的,IMO。使用枚举(.Net)代替,使代码更易读,更易于用于其他开发人员。此外,您不必在数字和描述上维护表格。

    也; try-catch-finally模式在我的书中是一种反模式。尝试 - 终于可以是好的,尝试捕获也可以是好的,但尝试捕获 - 终于永远不会好。 try-finally通常可以被"using"语句(IDispose模式)替换,这是更好的IMO。而实际捕获你能够处理异常的Try-catch是好的,或者如果你这样做:

    1
    2
    3
    4
    5
    6
    7
    try{
        db.UpdateAll(somevalue);
    }
    catch (Exception ex) {
        logger.Exception(ex,"UpdateAll method failed");
        throw;
    }

    所以只要你让异常继续泡沫就可以了。另一个例子是:

    1
    2
    3
    4
    5
    6
    7
    try{
        dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
    }
    catch (ConnectionException ex) {
        logger.Exception(ex,"Connection failed");
        dbHasBeenUpdated = false;
    }

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

    为什么然后尝试捕捉 - 终于反模式?原因如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try{
        db.UpdateAll(somevalue);
    }
    catch (Exception ex) {
        logger.Exception(ex,"UpdateAll method failed");
        throw;
    }
    finally {
        db.Close();
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try{
        using(IDatabase db = DatabaseFactory.CreateDatabase()) {
            db.UpdateAll(somevalue);
        }
    }
    catch (Exception ex) {
        logger.Exception(ex,"UpdateAll method failed");
        throw;
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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美分! :)


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

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


    我在Exceptional和非Exceptional情况下都在python中使用Exceptions。

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

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

    副作用是当运行datastore.fetch(obj_id)时,您永远不必检查其返回值是否为None,您可以立即免费获得该错误。这与论点相反,"你的程序应该能够执行所有主要功能而不使用异常"。

    下面是另一个示例,其中异常"异常"有用,以便编写用于处理不受竞争条件影响的文件系统的代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 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在更多情况下失败而不是目录不存在,但对于许多严格控制的情况来说,这是一个"足够好"的解决方案。


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


    我从实用程序员那里得到的一条很棒的建议就是"你的程序应该能够执行所有主要功能而不使用异常"。


    我更喜欢使用异常进行错误处理并返回值(或参数)作为函数的正常结果。这提供了一种简单而一致的错误处理方案,如果正确完成,它可以使代码更加清晰。


    其中一个很大的区别是异常会强制您处理错误,而错误返回代码可能会被取消选中。

    错误返回代码,如果大量使用,也会导致非常难看的代码,如果测试类似于此形式:

    1
    2
    3
    4
    5
    6
    if(function(call) != ERROR_CODE) {
        do_right_thing();
    }
    else {
        handle_error();
    }

    就个人而言,我更喜欢使用调用代码应该或必须对其执行的错误的异常,并且只使用错误代码来表示"预期失败",其中返回的内容实际上是有效且可能的。


    有很多理由更喜欢Exceptions而不是返回代码:

    • 通常,为了便于阅读,人们会尝试最小化方法中的return语句数。这样做,异常会阻止在非结合状态下执行一些额外的工作,从而防止可能损坏更多数据。
    • 异常通常比返回值更容易扩展,更容易扩展。假设一个方法返回自然数,并且当发生错误时使用负数作为返回码,如果你方法的范围改变并且现在返回整数,你将不得不修改所有方法调用而不是仅仅调整一点例外。
    • 异常允许更容易地分离正常行为的错误处理。它们允许确保某些操作以某种方式执行原子操作。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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?
      }
    }

    或者甚至更糟的是,如果我删除了一些我不应该拥有的东西,但是在我完成剩余的清理工作之前就抓住了。投掷给很差的用户恕我直言。


    我只使用例外,没有返回代码。我在这里谈论Java。

    我遵循的一般规则是,如果我有一个名为doFoo()的方法,那么如果它不是"do foo",那么就会发生异常事件,并抛出异常。


    我有一套简单的规则:

    1)使用返回代码来处理您希望直接调用者做出反应的事情。

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

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


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

    无论如何,如果可能,您必须处理错误。您可以轻松地将异常传播到顶层,因为忽略返回代码并让程序发生段错误。

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


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

    • 在需要本地化/国际化时使用错误代码 - 在.NET中,您可以使用这些错误代码来引用资源文件,然后以适当的语言显示错误。否则,请使用例外
    • 仅将异常用于非常特殊的错误。如果它经常发生,可以使用布尔值或枚举错误码。

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

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


    推荐阅读