假设我有这样的伪代码:

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 conditionA = executeStepA();
  if (!conditionA) return; // break
  bool conditionB = executeStepB();
  if (!conditionB) return; // break
  bool conditionC = executeStepC();
  if (!conditionC) return; // break
}();
executeThisFunctionInAnyCase();

我们创建一个带有隐式引用捕获的匿名lambda函数,并运行它。其中的代码立即运行。

当它想要停止时,它只是返回。

然后,在它运行之后,我们运行executeThisFunctionInAnyCase。

在lambda内的Return是到块结束的断点。任何其他类型的流控制都可以工作。

异常被单独保留——如果您想捕获它们,请显式地执行。在抛出异常时要小心运行executeThisFunctionInAnyCase——如果executeThisFunctionInAnyCase可以在异常处理程序中抛出异常,则通常不希望运行它,因为这会导致混乱(这种混乱取决于语言)。

这种基于捕获的内联函数的一个很好的特性是您可以重构现有的代码。如果您的函数非常长,那么将其分解为组件是个好主意。

有一种变体适用于更多语言:

bool working = executeStepA();
working = working && executeStepB();
working = working && executeStepC();
executeThisFunctionInAnyCase();

你写的每一行都短路。代码可以在这些行之间注入,给你多个“在任何情况下”,或者你可以在执行步骤之间做if(working) {/* Code */},包括当且仅当你还没有跳出时应该运行的代码。

对于这个问题,一个好的解决方案应该在添加新的流量控制时具有鲁棒性。

在c++中,一个更好的解决方案是创建一个快速的scope_guard类:

#ifndef SCOPE_GUARD_H_INCLUDED_
#define SCOPE_GUARD_H_INCLUDED_
template<typename F>
struct scope_guard_t {
  F f;
  ~scope_guard_t() { f(); }
};
template<typename F>
scope_guard_t<F> scope_guard( F&& f ) { return {std::forward<F>(f)}; }
#endif

然后在问题代码中:

auto scope = scope_guard( executeThisFunctionInAnyCase );
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

scope的析构函数自动运行executeThisFunctionInAnyCase。当您创建一个需要清理的非raii资源时,您可以在作用域末端注入更多这样的“资源”(给每个资源一个不同的名称)。它也可以取lambda,所以你可以操作局部变量。

更花哨的作用域保护可以支持终止析构函数中的调用(使用bool保护),阻止/允许复制和移动,并支持可以从内部上下文返回的类型擦除的“便携式”作用域保护。

其他回答

只做

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

就是这么简单。


由于三次编辑都从根本上改变了问题(如果算上版本1的修订,则是四次),我包括了我正在回答的代码示例:

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

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时,右边永远不会被计算。

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

代码中的IF/ELSE链不是语言的问题,而是程序设计的问题。如果你能够重构或重写你的程序,我建议你在Design Patterns (http://sourcemaking.com/design_patterns)中寻找更好的解决方案。

通常,当您在代码中看到大量IF & else时,就有机会实现策略设计模式(http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net)或其他模式的组合。

我确信有替代方案来编写一长串if/else,但我怀疑它们除了对你来说看起来很漂亮之外不会改变任何东西(然而,情人眼里出佳人仍然适用于代码:-))。你应该关心这样的事情(6个月后,当我有一个新的条件,我不记得关于这个代码的任何事情,我是否能够轻松地添加它?)或者如果链发生变化,我将如何快速和无错误地实现它)

在我看来,函数指针是最好的方法。

这种方法之前已经提到过,但是我想更深入地讨论一下对箭头类型的代码使用这种方法的优点。

根据我的经验,这种if链发生在程序的某个操作的初始化部分。程序在尝试启动之前需要确保一切正常。

通常情况下,在许多do stuff函数中一些东西可能会被分配,或者所有权可能会改变。如果你失败了,你会想要反转这个过程。

假设你有以下3个函数:

bool loadResources()
{
   return attemptToLoadResources();
}
bool getGlobalMutex()
{
   return attemptToGetGlobalMutex();
}
bool startInfernalMachine()
{
   return attemptToStartInfernalMachine();
}

所有函数的原型将是:

typdef bool (*initializerFunc)(void);

如上所述,您将使用push_back将指针添加到一个向量中,并按顺序运行它们。但是,如果您的程序在startInfernalMachine上失败,您将需要手动返回互斥量并卸载资源。如果在RunAllways函数中执行此操作,则会遇到麻烦。

但是等等!函子是非常棒的(有时),你可以只改变原型如下:

typdef bool (*initializerFunc)(bool);

为什么?好的,新函数现在看起来像这样:

bool loadResources(bool bLoad)
{
   if (bLoad)
     return attemptToLoadResources();
   else
     return attemptToUnloadResources();
}
bool getGlobalMutex(bool bGet)
{
  if (bGet)
    return attemptToGetGlobalMutex();
  else
    return releaseGlobalMutex();
}
...

所以现在,整个代码看起来就像这样:

vector<initializerFunc> funcs;
funcs.push_back(&loadResources);
funcs.push_back(&getGlobalMutex);
funcs.push_back(&startInfernalMachine);
// yeah, i know, i don't use iterators
int lastIdx;
for (int i=0;i<funcs.size();i++)
{
   if (funcs[i](true))
      lastIdx=i;
   else 
      break;
}
// time to check if everything is peachy
if (lastIdx!=funcs.size()-1)
{
   // sad face, undo
   for (int i=lastIdx;i>=0;i++)
      funcs[i](false);
}

因此,自动清理项目绝对是向前迈出的一步,并通过这个阶段。 然而,实现有点尴尬,因为您需要反复使用这个推回机制。如果你只有一个这样的位置,我们说它是可以的,但如果你有10个位置,有一个振荡的函数数量……这可不好玩。

幸运的是,还有另一种机制可以让您实现更好的抽象:可变函数。 毕竟,有许多不同数量的函数需要仔细研究。 变进函数是这样的:

bool variadicInitialization(int nFuncs,...)
{
    bool rez;
    int lastIdx;
    initializerFunccur;
    vector<initializerFunc> reverse;
    va_list vl;
    va_start(vl,nFuncs);
    for (int i=0;i<nFuncs;i++)
    {
        cur = va_arg(vl,initializerFunc);
        reverse.push_back(cur);
        rez= cur(true);
        if (rez)
            lastIdx=i;
        if (!rez)
            break;
    }
    va_end(vl);

    if (!rez)
    {

        for (int i=lastIdx;i>=0;i--)
        {
            reverse[i](false);
        }
    }
    return rez;
}

现在你的代码将被缩减(在应用程序的任何地方)为:

bool success = variadicInitialization(&loadResources,&getGlobalMutex,&startInfernalMachine);
doSomethingAllways();

通过这种方式,你可以用一个函数调用来完成所有那些讨厌的if列表,并确保当函数退出时,你不会有任何初始化的残留物。

您的团队成员将非常感激您在1行代码中实现了100行代码。

但是等等! 还有更多! 箭头类型代码的主要特征之一是需要有特定的顺序! 并且这个特定的顺序在整个应用程序中需要是相同的(多线程死锁避免规则1:在整个应用程序中始终以相同的顺序接受互斥) 如果有一个新来者,把函数按随机顺序排列呢?更糟糕的是,如果要求您将其公开给java或c#,该怎么办?(是的,跨平台是一种痛苦)

幸运的是,有一种方法可以解决这个问题。 以下是我的建议:

创建一个枚举,从第一个资源开始到最后一个资源 定义一个pair,从枚举中获取一个值,并将其与函数指针配对 把这些对放在一个向量中(我知道,我只是定义了一个映射的使用:),但我总是用向量表示小的数字) 将可变宏从函数指针改为整数(这很容易在java或c#中公开;))) 在变进函数中,对这些整数进行排序 运行时,运行分配给该整数的函数。

最后,您的代码将确保以下内容:

只要一行代码就可以初始化,不管需要多少东西 强制执行调用顺序:你不能在loadResources之前调用startInfernalMachine,除非你(架构师)决定允许这样做 如果在此过程中某些事情失败,则完成清理(考虑到您正确地进行了反初始化) 改变整个应用程序中初始化的顺序只意味着改变枚举中的顺序

另一种方法是do - while循环,尽管之前提到过,但没有例子可以显示它的样子:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(好吧,while循环已经有了一个答案,但do - while循环不会冗余检查true(在开始时),而是在结束时检查xD(尽管可以跳过))。