假设我有这样的伪代码:

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 (false);

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

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

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

其他回答

对于c++ 11及以上版本,一个很好的方法可能是实现一个类似D的作用域(退出)机制的作用域退出系统。

实现它的一种可能的方法是使用c++ 11 lambdas和一些helper宏:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

这将允许你提前从函数返回,并确保你定义的任何清理代码总是在作用域退出时执行:

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

宏实际上只是装饰。MakeScopeExit()可以直接使用。

在这种情况下,老式的C程序员使用goto。这是goto的一种用法,实际上是Linux样式指南鼓励的,它被称为集中函数exit:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

有些人使用goto的方法是将body包装成一个循环并将其断开,但实际上这两种方法做的是同一件事。如果你只在executeStepA()成功时才需要一些其他的清理,那么goto方法会更好:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

使用循环方法,在这种情况下,您将得到两级循环。

关于当前的代码示例,本质上是第二个问题,

[...block of code...]
bool conditionA = executeStepA();    
if (conditionA){
    [...block of code...]
    bool conditionB = executeStepB();
    if (conditionB){
        [...block of code...]
        bool conditionC = executeStepC();
        if (conditionC){
            ...other checks again...
        }
    }
}

executeThisFunctionInAnyCase();

除了将函数结果存储在变量中之外,这是典型的C代码。

如果布尔函数导致信号失败,那么c++的方法是使用异常,并将其编码为

struct Finals{ ~Finals() { executeThisFunctionInAnyCase(); } };

Finals finals;
// [...block of code...]
executeStepA();
// [...block of code...]
executeStepB();
// [...block of code...]
executeStepC();
//...other checks again...

然而,根据实际问题的不同,细节可能会有很大差异。

当我需要这样的通用最终操作时,我通常使用通用范围保护类,而不是当场定义一个自定义结构。作用域保护是由Petru Marginean为c++ 98发明的,然后使用临时生命周期扩展技巧。在c++ 11中,一般的范围保护类可以基于提供lambda表达式的客户端代码简单地实现。

在问题的最后,你建议了一个很好的C方法来做到这一点,即使用break语句:

for( ;; ) // As a block one can 'break' out of.
{
    // [...block of code...]
    if( !executeStepA() ) { break; }
    // [...block of code...]
    if( !executeStepB() ) { break; }
    // [...block of code...]
    if( !executeStepC() ) { break; }
    //...other checks again...
    break;
}
executeThisFunctionInAnyCase();

或者,对于C,将代码块中的代码重构为一个单独的函数,并使用return而不是break。因为它支持嵌套循环或开关,所以这更清楚也更通用。然而,你问的是休息。

与基于异常的c++方法相比,这种方法依赖于程序员记得检查每个函数的结果,并做正确的事情,这两者在c++中都是自动化的。

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

这是一个箭头

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

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

似乎你想在一个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();