【问题标题】:Are do-while-false loops common?do-while-false 循环是否常见?
【发布时间】:2009-09-11 16:51:00
【问题描述】:

不久前,我改变了处理 c 样式错误的方式。

我发现我的很多代码都是这样的:

int errorCode = 0;

errorCode = doSomething();
if (errorCode == 0)
{
   errorCode = doSomethingElse();
}

...

if (errorCode == 0)
{
   errorCode = doSomethingElseNew();
}

但最近我一直是这样写的:

int errorCode = 0;

do
{       
   if (doSomething() != 0) break;
   if (doSomethingElse() != 0) break;
   ...
   if (doSomethingElseNew() != 0) break;
 } while(false);

我见过很多代码在出现错误后什么都不执行,但它总是以第一种风格编写的。有没有其他人使用这种风格,如果你没有,为什么?

编辑: 澄清一下,通常这个构造使用errno,否则我会在中断之前将值分配给int。此外,if (error == 0 ) 子句中的代码通常不仅仅是单个函数调用。不过,有很多值得思考的好点。

【问题讨论】:

  • 顺便说一句,while(false) 给出了编译器警告。
  • 哪个编译器?哪种语言?
  • 可能是 GCC,因为 truefalse 在 C++ 中是保留字,但在 C 中不是。
  • 如果您的编译器抱怨,请将 false 替换为 0。它是伪 C/C++。
  • MSVC 还抱怨条件表达式在 W4 上保持不变。

标签: c++ c coding-style


【解决方案1】:

如果您使用的是 C++,请使用异常。如果您使用的是 C,则第一种风格效果很好。但是,如果您确实想要第二种样式,只需使用 gotos - 这正是 gotos 真正是最清晰构造的情况类型。

    int errorCode = 0;

    if ((errorCode = doSomething()) != 0) goto errorHandler;
    if ((errorCode = doSomethingElse()) != 0) goto errorHandler;
      ...
    if ((errorCode = doSomethingElseNew()) != 0) goto errorHandler;

    return;
errorHandler:
    // handle error

是的,goto 可能不好,每次调用后的异常或显式错误处理可能会更好,但 goto 比选择另一个构造来尝试和模拟它们要好得多。使用 gotos 还可以轻松地为特定错误添加另一个错误处理程序:

    int errorCode = 0;

    if ((errorCode = doSomething()) != 0) goto errorHandler;
    if ((errorCode = doSomethingElse()) != 0) goto errorHandler;
      ...
    if ((errorCode = doSomethingElseNew()) != 0) goto errorHandlerSomethingElseNew;

    return;
errorHandler:
    // handle error
    return;
errorHandlerSomethingElseNew:
    // handle error
    return;

或者,如果错误处理更多的是“展开/清理你所做的事情”,你可以像这样构造它:

    int errorCode = 0;

    if ((errorCode = doSomething()) != 0) goto errorHandler;
    if ((errorCode = doSomethingElse()) != 0) goto errorHandler1;
      ...
    if ((errorCode = doSomethingElseNew()) != 0) goto errorHandler2;

errorHandler2:
    // clean up after doSomethingElseNew
errorHandler1:
    // clean up after doSomethingElse
errorHandler:
    // clean up after doSomething
    return errorCode;

这个习语给你的好处是不重复你的清理代码(当然,如果你使用 C++,RAII 会更干净地覆盖清理代码。

【讨论】:

  • +1 goto 名声不好,但在这种情况下它才有意义。
  • 如果你使用这个习惯用法来确保展开部分完成的工作,你通常会按照它们被调用的相反顺序排列错误标签。
  • 只要他检查的错误代码确实是“异常的”。异常适用于灾难性错误,不适用于工厂的正常运行条件。
【解决方案2】:

第二个 sn-p 看起来不对。您实际上是重新发明了 goto。

任何阅读第一种代码风格的人都会立即知道发生了什么,第二种风格需要更多检查,因此从长远来看会使维护更加困难,没有真正的好处。

编辑,在第二种风格中,您已经丢弃了错误代码,因此您无法采取任何纠正措施或显示信息性消息、记录有用的信息等......

【讨论】:

  • 不,goto 不同。在这种情况下,他只是跳出当前范围,因此不会出错。
  • @Glen:不,一点也不苛刻。这也是我首先想到的。 +1
  • @Shirkrin:如果你想要 goto,那么使用 goto - 不要通过暗示没有循环时存在循环来误导读者。
  • 是的,这是一个 goto。但是,我遇到过一个例子,GOTO 是正确的解决方案。 (一。大约八年后。)然而,这不是它。
  • 转到?同样的逻辑,“if”、“for”和函数调用也是“美化 goto”。我们为成熟的“goto”添加了限制以使其更可用——“break”就是这样的限制之一。 “do”可能会产生误导(循环?),但“goto”也会产生误导(跳转到任何地方?)。 “中断”具有特定含义:转义一层上下文。
【解决方案3】:

第一种样式是经验丰富的眼睛立即摸索的图案。

第二个需要更多的思考 - 你看着它并看到一个循环。您期望进行多次迭代,但是当您阅读它时,这种心理模型会被打破......

当然,它可能会起作用,但编程语言不仅仅是一种告诉计算机该做什么的方式,它们也是一种将这些想法传达给其他人的方式。

【讨论】:

  • 这不是一个无限循环。这是怪事的一部分。这是一个只会执行一次的 do while 循环。他使用休息来跳过他不想执行的方法。当然,您确实错过了这种微妙之处的事实是不使用它的好理由。如果经验丰富的开发人员可以错过这一点,那么经验不足的开发人员还有什么希望呢。
  • 不要试图暗示你有任何不好的地方。只是想说明为什么我们应该尽量减少晦涩难懂的编码技巧。
  • 在你写评论之前我已经更正了,但你是对的,它只是用来说明第二种模式导致的那种认知失调!
  • 我认为您永远不想在代码中设置期望,然后在一段时间后打破它们。 “do {} while (false);”的任何使用应该在评论中预先标记,或者最好将其转换为其他内容。
【解决方案4】:

我认为第一个可以让您更好地控制如何处理特定错误。第二种方式只告诉您发生了错误,而不是告诉您错误发生在哪里或发生了什么。

当然,例外优于两者……

【讨论】:

  • 使用异常会很棒。但是我们仍然必须使用不使用异常的代码,例如系统 API、dlclose 等。因此拥有一个处理这些错误代码的好系统很重要
  • Joel Spolsky 抱怨异常是不可见的 goto 在这里:joelonsoftware.com/articles/Wrong.html
  • 但是想象一下..如果你不想写c也不例外?!
  • @Shirkrin - 你可以有“例外”,你只需要使用 goto 来模拟它们。请参阅 Eclipse 的答案。然后看看 Joel Spolsky 反对首先使用异常的论点,正如 Glenn 所引用的那样。
  • 我讨厌例外。良好的可靠返回代码。我们中的一些人检查他们你知道的。
【解决方案5】:

让它简短、紧凑且易于快速阅读?

怎么样:

if ((errorcode = doSomething()) == 0
&&  (errorcode = doSomethingElse()) == 0
&&  (errorcode = doSomethingElseNew()) == 0)
    maybe_something_here;
return errorcode; // or whatever is next

【讨论】:

  • 也许不是最容易阅读的东西,但内置的短路行为正是我们想要的。
【解决方案6】:

为什么不用函数替换 do/while 和 break 并返回?

你重新发明了 goto。

【讨论】:

  • 我猜因为这不是 C++,所以可能需要在返回之前进行手动清理。
  • 可能有代码在“循环”之后被执行。 “循环”可以做成它自己的函数,但是用返回而不是中断。
【解决方案7】:

如何使用异常?

try {
  DoSomeThing();
  DoSomethingElse();
  DoSomethingNew();
  .
  .
  .
}
catch(DoSomethingException e) {
  .
  .
}
catch(DoSomethingElseException e) {
  .
  .
}
catch(DoSomethingNewException e) {
  .
  .
}
catch(...) {
  .
  .
}

【讨论】:

  • 如果我们调用的每个方法都抛出而不是返回异常,那就太好了。但是,我们无法控制我们调用的每个方法。大多数系统 API 使用 errorCode 而不是异常
  • @Glen: if ( !(error = FunctionCall()) ) throw CustomException(error);
  • @Paul:实际上,例外情况更像 COMEFROM。
  • @David:因为这样更好。 ;-)
  • @Paul - 我同意这是您在不使用 G 字的情况下最接近 GOTO,但作为开发自然语言编译器的人 - 有时 GOTO 是获得什么的最快、最简单、最清晰的方法你需要。被学院嘲笑并不会降低它的用处:) @Shog9 - 很好的解决方案!
【解决方案8】:

你的方法不是很糟糕,也不是像这里的人声称的那样难以理解,但它是非传统的,会惹恼一些人(正如你在这里注意到的那样)。

在您的代码达到一定大小后,第一个会变得非常烦人,因为它有很多样板。

当我不能使用异常时,我倾向于使用的模式更像是:

fn() {
    while(true) {
        if(doIt())
            handleError();//error detected...
    }
}

bool doIt() {
    if(!doThing1Succeeds())
        return true;
    if(!doThing2Succeeds())
        return true;
    return false;
}

如果您将正确的魔法咒语放入签名中,您的第二个函数应该内联到第一个函数中,并且每个函数都应该更具可读性。

这在功能上与没有非常规语法的 while/bail 循环相同(而且更容易理解,因为您将循环/错误处理的关注点与“您的程序在给定循环中做什么”的关注点分开”。

【讨论】:

    【解决方案9】:

    这应该通过异常来完成,至少在 C++ 标记正确的情况下。如果您只使用 C,则没有任何问题,尽管我建议您使用布尔值,因为您没有使用返回的错误代码。您也不必输入 != 0...

    【讨论】:

    • 如果我们在 C++ 中调用的每个方法都抛出而不是返回异常,那就太好了。但是,我们无法控制我们调用的每个方法。大多数系统 API 使用 errorCode 而不是异常
    【解决方案10】:

    我已经在几个地方使用过这种技术(所以你不是唯一一个这样做的人)。但是,我一般不会这样做,并且在我使用它的地方我对它的感觉很复杂。在一些地方与仔细的文档(cmets)一起使用,我可以接受。随处使用 - 不,通常不是一个好主意。

    相关展品:来自SQLCMD 源代码的文件 sqlstmt.ec、upload.ec、reload.ec(不是,不是微软的冒名顶替者;我的)。 '.ec' 扩展名意味着该文件包含 ESQL/C - C 中的嵌入式 SQL,它被预处理为纯 C;您无需了解 ESQL/C 即可查看循环结构。循环都标有:

        /* This is a one-cycle loop that simplifies error handling */
    

    【讨论】:

      【解决方案11】:

      经典的 C 习语是:

      if( (error_val = doSomething()) == 0)
      { 
         //Manage error condition
      }
      

      请注意,C 从赋值中返回赋值,从而可以执行测试。人们通常会写:

      if( ! ( error_val = doSomething()))
      

      但为了清楚起见,我保留了== 0

      关于你的习语...

      你的第一个成语没问题。你的第二个习语是滥用语言,你应该避免它。

      【讨论】:

      • 我个人更喜欢在不同的行上进行分配和检查,因为如果我不这样做,它会开始看起来很混乱(可能是因为我不喜欢括号之间有空格。我不想有多于一对括号,无论如何)。
      • 我并不是说这是唯一的真道。这只是经典的成语。我通常也将分配和检查分成两个块 - 随着代码的发展,似乎效果更好。
      【解决方案12】:

      那么这个版本怎么样

      我通常会像您的第一个示例那样做一些事情,或者可能使用这样的布尔值:

      bool statusOkay = true;
      
      if (statusOkay)
          statusOkay = (doSomething() == 0);
      
      if (statusOkay)
          statusOkay = (doSomethingElse() == 0);
      
      if (statusOkay)
          statusOkay = (doSomethingElseNew() == 0);
      

      但如果您真的热衷于第二种技术的简洁性,那么您可以考虑这种方法:

      bool statusOkay = true;
      
      statusOkay = statusOkay && (doSomething() == 0);
      statusOkay = statusOkay && (doSomethingElse() == 0);
      statusOkay = statusOkay && (doSomethingElseNew() == 0);
      

      只是不要指望维护程序员会感谢你!

      【讨论】:

        【解决方案13】:

        我偶尔会在合适的时候使用do { } while (false);。我认为它类似于 try/catch 块,因为我将代码设置为一个块,其中包含一系列可能存在异常的决策,并且需要通过规则和逻辑的各种路径在最后合并块。

        我很确定我只在 C 编程中使用这种结构,而且并不经常使用。

        对于您给出的一系列函数调用的具体示例,这些函数调用将一个接一个地执行,完成整个系列或如果检测到错误则系列停止,我可能只使用 if 语句检查错误变量.

        {
            int iCallStatus = 0;
            iCallStatus = doFunc1();
            if (iCallStatus == 0) iCallStatus = doFunc2();
            if (iCallStatus == 0) icallStatus = doFunc3();
        }
        

        这很短,即使没有cmets,意思也很简单明了。

        我不时遇到的是,这种相当简单的程序步骤顺序流程不适用于特定要求。我需要的是创建一个包含各种决策的代码块,通常涉及循环或迭代一些数据对象系列,我想将此系列视为一种事务,如果没有错误或中止,事务将被提交如果在事务处理过程中发现某种错误情况。作为这个数据块的一部分,我可能会为do { } while (false); 的范围创建一组临时变量,当我使用它时,我总是会在注释中指出这是一个单次迭代,比如:

        do {  // single loop code block begins
            // block of statements for business logic with single ending point
        } while (false);  // single loop code block ends
        

        当我发现自己认为这种构造是必要的时,我会查看代码是否需要重构,或者一个函数或一组函数是否更合适。

        我更喜欢这种结构而不是使用goto 语句的原因是括号和缩进的使用使源代码更易于阅读。使用我的编辑器,我可以轻松找到块的顶部和底部,并且缩进使将代码可视化为具有单个入口点和已知结束点的块变得更容易。街区内可能有多个出口点,但我知道它们最终会在哪里结束。使用这意味着我可以创建超出范围的本地化变量,尽管只使用没有do { } while (false); 的括号也可以做到这一点。但是我使用 do while 因为我需要休息;能力也是如此。

        我会考虑在以下某些情况下使用这种样式。如果正在实施的业务逻辑需要一组变量,这些变量由重新加入的不同可能执行路径共享和引用。如果业务逻辑复杂,具有多个状态,并使用多个级别的 if 语句进行检查,并且如果在处理过程中检测到错误,则会设置错误指示并中止处理。

        当我使用它时,我能想到的唯一一次是有点粗糙,这有助于澄清并使处理中止更容易。所以基本上我使用这个类似于使用 try/catch 抛出异常。

        【讨论】:

        • 您需要 cmets 应该是一个直接的危险信号,代码将难以维护。
        【解决方案14】:

        第一种风格很好地说明了异常为何优越:您甚至看不到算法,因为它隐藏在显式错误处理之下。

        第二种样式滥用循环构造来模仿goto,以误导试图避免必须明确拼出goto。这显然是邪恶的,长期使用会导致你走向黑暗的一面。

        【讨论】:

          【解决方案15】:

          对我来说,我更喜欢:

          if(!doSomething()) {
              doSomethingElse();
          }
          doSomethingNew();
          

          所有其他的东西都是掩盖三个函数调用的语法噪音。在 Else 和 New 中,您可以抛出错误,或者如果较旧,则使用 longjmp 回到之前的一些处理。不错,干净而且相当明显。

          【讨论】:

            【解决方案16】:

            这里似乎存在比您的控制结构更深层次的问题。为什么你有如此复杂的错误控制? IE。你似乎有多种方法来处理不同的错误。

            通常,当我遇到错误时,我只是中断操作,向用户显示错误消息,并将控制权返回给事件循环(如果是交互式应用程序)。对于批处理,记录错误,然后继续处理下一个数据项或中止处理。

            这种控制流很容易处理异常。

            如果您必须处理错误编号,那么您可以通过在发生错误时继续正常的错误处理或返回第一个错误来有效地模拟异常。您在发生错误后继续处理似乎非常脆弱,或者您的错误条件实际上是控制条件而不是错误条件。

            【讨论】:

              【解决方案17】:

              老实说,我所知道的更有效的 C/C++ 程序员只会在这种情况下使用 goto。一般的方法是使用单个退出标签,然后进行所有清理。该函数只有一个返回路径。当清理逻辑开始变得复杂/有条件时,将函数分解为子函数。这对于使用 C/C++ imo 进行系统编码非常典型,您调用的 API 返回错误代码而不是抛出异常。

              一般来说,goto 是不好的。由于我所描述的用法非常普遍,因此我认为始终如一地使用它很好。

              【讨论】:

                【解决方案18】:

                第二种风格通常用于在 C 中管理资源分配和解除分配,RAII 无法解决问题。 通常,您会在执行之前声明一些资源,在伪循环内部分配和使用它们,并在外部取消分配它们。

                一般范式的一个例子如下:

                int status = 0;
                
                // declare and initialize resources
                BYTE *memory = NULL;
                HANDLE file = INVALID_HANDLE_VALUE;
                // etc...
                
                do
                {
                  // do some work
                
                  // allocate some resources
                  file = CreateFile(...);
                  if(file == INVALID_HANDLE_VALUE)
                  {
                    status = GetLastError();
                    break;
                  }
                
                  // do some work with new resources
                
                  // allocate more resources
                
                  memory = malloc(...);
                  if(memory == NULL)
                  {
                    status = ERROR_OUTOFMEMORY;
                    break;
                  }
                
                  // do more work with new resources
                } while(0);
                
                // clean up the declared resources
                if(file != INVALID_HANDLE_VALUE)
                  CloseHandle(file);
                
                if(memory != NULL)
                  free(memory);
                
                return status;
                

                话虽如此,RAII 以更简洁的语法解决了同样的问题(基本上,您可以完全忘记清理代码)并处理这种方法无法处理的一些场景,例如异常。

                【讨论】:

                  【解决方案19】:

                  我以前见过这种模式,但不喜欢它。通常,可以通过将逻辑拉入单独的函数来清理它。

                  代码就变成了

                      ...
                      int errorCode = doItAll();
                      ...
                  
                  
                      int doItAll(void) {
                        int errorCode;
                        if(errorCode=doSomething()!=0)
                          return errorCode;
                        if(errorCode=doSomethingElse()!=0)
                          return errorCode;
                        if(errorCode=doSomethingElseNew()!=0)
                          return errorCode;
                        return 0;
                      }
                  

                  将此与清理结合起来也变得非常容易,您只需使用 goto 和错误处理程序,就像在 eclipses 答案中一样。

                      ...
                      int errorCode = doItAll();
                      ...
                  
                  
                      int doItAll(void) {
                        int errorCode;
                        void * aResource = NULL; // Somthing that needs cleanup after doSomethingElse has been called
                        if(errorCode=doSomething()!=0) //Doesn't need cleanup
                          return errorCode;
                        if(errorCode=doSomethingElse(&aResource)!=0)
                          goto cleanup;
                        if(errorCode=doSomethingElseNew()!=0)
                          goto cleanup;
                        return 0;
                      cleanup:
                        releaseResource(aResource);
                        return errorCode;
                      }
                  

                  【讨论】:

                    【解决方案20】:

                    我在管理指针分配时使用第二种方法,并且当函数可以接受失败时,抛出异常并不是真正的正确答案。

                    我发现在一个地方而不是多个地方管理指针的清理更容易,而且你知道它只会在一个地方返回。

                    
                    pointerA * pa = NULL;
                    pointerB * pb = NULL;
                    pointerB * pc = NULL;
                    BOOL bRet = FALSE;
                    pa = new pointerA();
                    do {
                      if (!dosomethingWithPA( pa ))
                        break;
                       pb = new poninterB();
                      if(!dosomethingWithPB( pb ))
                        break;
                      pc = new pointerC();
                      if(!dosemethingWithPC( pc ))
                        break;
                      bRet = TRUE;
                    } while (FALSE);
                    
                    //
                    // cleanup
                    //
                    if (NULL != pa)
                     delete pa;
                    if (NULL != pb)
                     delete pb;
                    if (NULL != pc)
                     delete pc;
                    
                    return bRet;
                    
                    

                    对比

                    
                    pointerA * pa = NULL;
                    pointerB * pb = NULL;
                    pointerB * pc = NULL;
                    
                    

                    pa = new pointerA(); if (!dosomethingWithPA( pa )) { delete pa; return FALSE; }

                    pb = new poninterB(); if(!dosomethingWithPB( pb )) { delete pa; delete pb; return FALSE; } pc = new pointerC(); if(!dosemethingWithPAPBPC( pa,pb,pc )) { delete pa; delete pb; delete pc; return FALSE; }

                    delete pa; delete pb; delete pc; return TRUE;

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2010-11-02
                      • 2012-02-29
                      • 2011-08-28
                      • 2018-05-09
                      • 2015-07-03
                      • 1970-01-01
                      • 2016-02-21
                      • 1970-01-01
                      相关资源
                      最近更新 更多