do-while-false循环是常见的吗?

时间:2009-09-11 16:51:00

标签: c++ c coding-style

前段时间我改变了我处理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 )子句中的单个函数调用。不过要考虑很多好点。

20 个答案:

答案 0 :(得分:54)

如果您使用的是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

是的getos可能很糟糕,每次调用后的异常或显式错误处理可能会更好,但是比使用另一个构造来尝试和模拟它们要好得多。使用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 :(得分:46)

第二个片段看起来不错。你有效地重新发明了。

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

编辑,在第二种风格中,你已经丢掉了错误代码,因此你不能采取任何纠正措施或显示信息性消息,记录有用的东西等....

答案 2 :(得分:16)

第一种风格是经验丰富的眼睛同时出现的模式。

第二个需要更多思考 - 你看看它并看到一个循环。你期待几次迭代,但是当你读完它时,这个心理模型会被粉碎......

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

答案 3 :(得分:11)

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

当然,例外优于两者......

答案 4 :(得分:8)

简短,紧凑,易于快速阅读?

怎么样:

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

答案 5 :(得分:4)

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

你重新改造了goto。

答案 6 :(得分:3)

如何使用例外?

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

答案 7 :(得分:3)

你的方法并不是很糟糕,并不像这里的人声称的那样难以理解,但这是非常规的,会让一些人烦恼(正如你在这里注意到的那样)。

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

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

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

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

如果你在签名中加入了正确的魔术咒语,你的第二个函数应该被内联到第一个函数中,并且每个函数应该更具可读性。

这在功能上与没有非常规语法的while / bail循环相同(也更容易理解,因为你将循环/错误处理的问题与“你的程序在给定循环中做了什么”的问题分开了”

答案 8 :(得分:2)

这应该通过异常来完成,至少在C ++标记是否正确的情况下。如果你只使用C,没有错,虽然我建议使用布尔代替,因为你没有使用返回的错误代码。你不必输入!= 0然后......

答案 9 :(得分:1)

我在一些地方使用过这种技术(所以你不是唯一一个这样做的人)。但是,作为一般规则,我不这样做,而且我对它的使用方式感到好奇。在一些地方使用仔细的文档(评论),我很好。在任何地方都使用 - 不,通常不是一个好主意。

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

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

答案 10 :(得分:1)

经典的C语言是:

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

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

if( ! ( error_val = doSomething()))

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

关于你的习语......

你的第一个习语是好的。你的第二个习语是滥用语言,你应该避免使用它。

答案 11 :(得分:1)

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

我很确定我只在C编程中使用这个构造,并且它不常用。

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

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

这很简短,即使没有评论,其含义也很简单明了。

我不时遇到的是,这种相当直接的程序步骤顺序流程并不适用于特定要求。我需要的是创建一个包含各种决策的代码块,通常涉及循环或迭代某些系列的数据对象,我想将此系列视为一种事务,如果没有错误或中止,事务将被提交如果在处理交易期间发现某种错误情况。作为这个数据块的一部分,我可以为do { } while (false);的范围创建一组临时变量。当我使用它时,我总是发表评论,表明这是一次迭代,其中包括:< / p>

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抛出异常。

答案 12 :(得分:1)

这个版本怎么样

我通常会像你的第一个例子那样做,或者像这样做一个布尔值:

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 :(得分:0)

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

然后代码变为

    ...
    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和错误处理程序。

    ...
    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;
    }

答案 14 :(得分:0)

第二种风格通常用于管理C中的资源分配和解除分配,其中RAII没有得到拯救。 通常,您可以在do之前声明一些资源,在伪循环中分配和使用它们,并在外部取消分配它们。

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

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用更清晰的语法解决了同样的问题(基本上,你可以完全忘记清理代码)并处理这种方法没有的一些场景,例如异常。

答案 15 :(得分:0)

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

一般来说,搞砸了。由于我所描述的用法非常普遍,所以我认为它很好。

答案 16 :(得分:0)

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

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

这种控制流程很容易处理,但有例外。

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

答案 17 :(得分:0)

对我来说,我更喜欢:

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

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

答案 18 :(得分:0)

第一种风格是异常优越的一个很好的例子:你甚至看不到算法,因为它隐藏在明确的错误处理之下。

第二种风格滥用循环结构来模仿goto的误导性尝试,以避免必须明确拼出goto。这显然是邪恶的,长时间的使用将导致你走向黑暗的一面。

答案 19 :(得分:-1)

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

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


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;