假设我有这样的伪代码:

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语句可以以某种方式使用吗?


当前回答

一个简单的解决方案是使用一个条件布尔变量,同一个变量可以重复使用,以检查顺序执行的所有步骤的结果:

    bool cond = executeStepA();
    if(cond) cond = executeStepB();
    if(cond) cond = executeStepC();
    if(cond) cond = executeStepD();

    executeThisFunctionInAnyCase();

并不是说在此之前没有必要这样做:bool cond = true;... 然后后跟if(cond) cond = executeStepA();cond变量可以立即赋值给executeStepA()的结果,因此使代码更短,更易于阅读。

另一个更奇特但有趣的方法是这样的(有些人可能认为这是IOCCC的一个很好的候选,但仍然如此):

    !executeStepA() ? 0 :
      !executeStepB() ? 0 :
      !executeStepC() ? 0 :
      !executeStepD() ? 0 : 1 ;

    executeThisFunctionInAnyCase();

结果是完全相同的,如果我们做什么OP张贴,即:

    if(executeStepA()){
        if(executeStepB()){
            if(executeStepC()){
                if(executeStepD()){
                }
            }
        }
    }

    executeThisFunctionInAnyCase();

其他回答

break语句可以以某种方式使用吗?

也许不是最好的解决办法,但你可以把你的陈述变成一种方式。While(0)循环并使用break语句而不是return。

另一种解决方案是通过宏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块将有不同的作用域。

把有条件的东西移到else中怎么样?

if (!(conditionA = executeStepA()){}
else if (!(conditionB = executeStepB()){}
else if (!(conditionC = executeStepC()){}
else if (!(conditionD = executeStepD()){}

这确实解决了缩进问题。

如果你不喜欢goto和do {} while (0);循环和喜欢使用c++,你也可以使用临时lambda来达到同样的效果。

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();

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

这是一个箭头

您正在讨论的内容被称为箭头反模式。它之所以被称为箭头,是因为嵌套的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时,右边永远不会被计算。

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