假设我有这样的伪代码:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

函数executeStepX当且仅当前一个成功时执行。 在任何情况下,executeThisFunctionInAnyCase函数都应该在最后被调用。 我在编程方面是一个新手,所以很抱歉提出一个非常基本的问题:有没有一种方法(例如在C/ c++中)以代码易读性为代价,避免长if链产生那种“金字塔式代码”?

我知道如果我们可以跳过executeThisFunctionInAnyCase函数调用,代码可以简化为:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

但是约束是executeThisFunctionInAnyCase函数调用。 break语句可以以某种方式使用吗?


当前回答

假循环已经提到了,但我没有看到下面的技巧在给出的答案:你可以使用一个do{/*…*/} while(evalates_to_zero ());实现一个双向早出中断。使用break终止循环而不计算条件语句,而使用continue将计算条件语句。

你可以使用它,如果你有两种类型的终结,其中一条路径必须比另一条路径做更多的工作:

#include <stdio.h>
#include <ctype.h>

int finalize(char ch)
{
    fprintf(stdout, "read a character: %c\n", (char)toupper(ch));

    return 0;
}

int main(int argc, char *argv[])
{
    int ch;
    do {
        ch = fgetc(stdin);
        if( isdigit(ch) ) {
            fprintf(stderr, "read a digit (%c): aborting!\n", (char)ch);
            break;
        }
        if( isalpha(ch) ) {
            continue;
        }
        fprintf(stdout, "thank you\n");
    } while( finalize(ch) );

    return 0;
}

执行此命令会得到以下会话协议:

dw@narfi ~/misc/test/fakeloopbreak $ ./fakeloopbreak 
-
thank you
read a character: -

dw@narfi ~/misc/test/fakeloopbreak $ ./fakeloopbreak 
a
read a character: A

dw@narfi ~/misc/test/fakeloopbreak $ ./fakeloopbreak 
1
read a digit (1): aborting!

其他回答

这是一种常见的情况,有许多常见的方法来处理它。以下是我对一个权威答案的尝试。请评论,如果我错过了什么,我会保持这篇文章的最新。

这是一个箭头

您正在讨论的内容被称为箭头反模式。它之所以被称为箭头,是因为嵌套的if链形成的代码块会越来越向右扩展,然后再向左扩展,形成一个可视的箭头,“指向”代码编辑器窗格的右侧。

用守卫压平箭

这里讨论了一些避免绿箭的常见方法。最常见的方法是使用保护模式,在这种模式下,代码首先处理异常流,然后处理基本流,例如代替

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... 你会使用……

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

当有一长串的守卫时,这会使代码变得相当平坦,因为所有的守卫都出现在左边,并且你的if没有嵌套。此外,您可以直观地将逻辑条件与其相关的错误配对,这使得更容易判断正在发生什么:

箭:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

警卫:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

这在客观和量化上更容易阅读,因为

给定逻辑块的{和}字符靠得更近 理解某句话所需要的心理语境的量更小了 与if条件相关的全部逻辑更有可能出现在一页上 编码器滚动页面/眼球轨迹的需要大大减少了

如何在末尾添加公共代码

这种防范模式的问题在于,它依赖于所谓的“机会主义回归”或“机会主义退出”。换句话说,它打破了每个函数都应该只有一个退出点的模式。这是一个问题,有两个原因:

它惹恼了一些人,例如,在Pascal上学习编码的人已经知道一个函数=一个出口点。 它没有提供一段在退出时执行的代码,这是手头的主题。

下面我提供了一些绕过这个限制的选项,可以使用语言特性,也可以完全避免这个问题。

选项1。你不能这样做:使用finally

不幸的是,作为c++开发人员,您不能这样做。但对于包含finally关键字的语言,这是最重要的答案,因为这正是它的用途。

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

第二个选项。避免这个问题:重组你的职能

可以通过将代码分解为两个函数来避免这个问题。这种解决方案的优点是适用于任何语言,此外,它可以降低圈复杂度,这是一种经过验证的降低缺陷率的方法,并提高任何自动化单元测试的特异性。

这里有一个例子:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

选项3。语言技巧:使用假循环

我看到的另一个常见的技巧是使用while(true)和break,如其他答案所示。

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

虽然这没有使用goto那么“诚实”,但它在重构时不太容易被搞砸,因为它清楚地标记了逻辑作用域的边界。一个天真的程序员剪切和粘贴标签或goto语句会导致严重的问题!(坦率地说,这种模式现在很常见,我认为它清楚地传达了意图,因此一点也不“不诚实”)。

这个选项还有其他变体。例如,你可以用switch代替while。任何带有break关键字的语言构造都可能工作。

选项4。利用对象生命周期

另一种方法利用对象生命周期。使用context对象来携带参数(这是我们简单的例子所缺乏的),并在完成后处理它。

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

注意:请确保您理解所选语言的对象生命周期。为此你需要某种确定性的垃圾收集,也就是说,你必须知道什么时候会调用析构函数。在某些语言中,您需要使用Dispose而不是析构函数。

选择4.1。利用对象生命周期(包装器模式)

如果您打算使用面向对象的方法,不妨做对了。这个选项使用一个类来“包装”需要清理的资源,以及它的其他操作。

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

再次强调,一定要理解对象的生命周期。

选择5。语言技巧:使用短路评估

另一种技术是利用短路评估。

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

这个解决方案利用了&&操作符的工作方式。当&&的左边被计算为false时,右边永远不会被计算。

当需要紧凑的代码时,当代码不太可能看到太多维护时,这个技巧是最有用的,例如你正在实现一个众所周知的算法。对于更通用的编码,这段代码的结构太脆弱了;即使是对逻辑的微小更改也可能引发完全重写。

在c++中(这个问题在C和c++中都有标记),如果你不能改变函数来使用异常,你仍然可以使用异常机制,如果你写一个像这样的小助手函数

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

然后你的代码可以如下所示:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

如果你喜欢花哨的语法,你可以通过显式强制转换来让它工作:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

然后您可以将代码编写为

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

另一种解决方案是通过宏hack定义习语。

 #define block for(int block = 0; !block; block++)

现在,“block”可以用break退出,与for(;;)和while()循环的方式相同。例子:

int main(void) {

    block {
       if (conditionA) {
          // Do stuff A...
          break; 
       }
       if (conditionB) {
          // Do stuff B...
          break; 
       }
       if (conditionC) {
          // Do stuff C...
          break; 
       }
       else {
         // Do default stuff...
       }
    } /* End of "block" statement */
    /* --->   The "break" sentences jump here */

    return 0;
} 

尽管使用了“for(;;)”结构,但“block”语句只执行了一次。 这些“块”可以用断句退出。 因此,if else if else if…避免使用句子。 最多,最后一个else可以挂在“块”的末尾,以处理“默认”情况。

该技术旨在避免典型的和丑陋的做{…} while(0)方法。 在宏块中,它定义了一个同样名为block的变量,该变量以这样一种方式定义,即恰好执行了一次for迭代。根据宏的替换规则,宏块定义中的标识符块不会被递归替换,因此block成为程序员无法访问的标识符,但在内部可以很好地控制for(;;)循环的“隐藏”。

此外:这些“块”可以嵌套,因为隐藏变量int块将有不同的作用域。

有几个回答暗示了我看到并使用过很多次的模式,尤其是在网络编程中。在网络堆栈中,经常有很长的请求序列,其中任何一个都可能失败并停止进程。

常见的模式是使用do {} while (false);

我使用宏while(false)使它做{}一次;常见的模式是:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

这种模式相对容易阅读,并且允许使用能够正确销毁的对象,还避免了多次返回,从而使步进和调试更容易一些。

似乎你想在一个block中完成所有的调用。 正如其他人所建议的那样,你应该使用while循环并使用break离开,或者使用一个可以使用return离开的新函数(可能更干净)。

我个人排斥goto,即使是函数退出。在调试时很难发现它们。

一个适合您的工作流的优雅替代方法是构建一个函数数组并在此基础上迭代。

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();