假设我有这样的伪代码:

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


当前回答

一种有趣的方法是处理异常。

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

如果您编写这样的代码,那么您就在某种程度上走错了方向。我不认为拥有这样的代码是“问题”,而是拥有如此混乱的“架构”。

提示:与您信任的经验丰富的开发人员讨论这些情况;-)

其他回答

只做

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

就是这么简单。


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

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

executeThisFunctionInAnyCase();

为什么没有人给出最简单的解决方案?: D

如果你所有的函数都有相同的签名,那么你可以这样做(对于C语言):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

对于简洁的c++解决方案,您应该创建一个接口类,其中包含一个执行方法,并将步骤包装在对象中。 然后,上面的解看起来像这样:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();

正如Rommik提到的,您可以为此应用设计模式,但我将使用Decorator模式而不是Strategy,因为您想要链式调用。如果代码很简单,那么我会选择一个结构良好的答案来防止嵌套。但是,如果它很复杂或者需要动态链接,那么Decorator模式是一个很好的选择。这是一个yUML类图:

下面是一个示例LinqPad c#程序:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

恕我直言,关于设计模式最好的书是《Head First design patterns》。

代码中的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链形成的代码块会越来越向右扩展,然后再向左扩展,形成一个可视的箭头,“指向”代码编辑器窗格的右侧。

用守卫压平箭

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

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

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