如何避免“如果”连锁?


266

假设我有这个伪代码:

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


254
@FrédéricHamidi错误错误错误!永远不要说带例外驱动程序流是好的!出于各种原因,异常绝对不适合此目的。
Piotr Zierhoffer 2014年

26
@Piotr,我被Python宠坏了(实际上鼓励这样做)。我知道不应该在C ++中将异常用于流控制,但是这里真的是流控制吗?函数返回false不能被视为类似于异常情况吗?
弗雷德里克·哈米迪

13
那取决于程序的语义。一个false回报可以是相当正常的。
dornhege 2014年

28
我已经将您的问题回滚到其第一版。收到一定数量的问题(> 0)后,您不应该从根本上更改问题,因为这将使到那时为止给出的所有答案无效,并且会造成混乱。而是打开一个新问题。
2014年

13
我希望所有“新手程序员”都会提出这样的设计问题。
Jezen Thomas

Answers:


486

您可以使用&&(逻辑与):

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

这将满足您的两个要求:

  • executeStep<X>()仅在上一个成功的情况下才应评估(这称为短路评估
  • executeThisFunctionInAnyCase() 无论如何都会执行

29
这在语义上是正确的(实际上,我们希望所有条件都成立),同时又是一种非常好的编程技术(短路评估)。此外,这可以在创建函数会使代码混乱的任何复杂情况下使用。
Sanchises

58
@RobAu:那么对于初级程序员来说,最终看到依赖于捷径评估的代码将是一件好事,并且可能会促使他们重新研究该主题,从而有助于他们最终成为高级程序员。因此很明显是双赢的:体面的代码,有人从阅读中学到了一些东西。
x4u 2014年

24
这应该是最好的答案
显示名称

61
@RobAu:这不是利用一些晦涩的语法技巧的黑客,它在几乎每种编程语言中都是非常惯用的,以至于成为无可争议的标准实践。
BlueRaja-Danny Pflughoeft 2014年

38
仅当条件实际上是简单函数调用时,此解决方案才适用。在实际代码中,这些条件可能是2-5行长(它们本身是许多其他条件&&和的组合||),因此,在不影响if可读性的情况下,您不可能将它们连接到单个语句中。而且,将这些条件移至外部函数并不总是那么容易,因为它们可能依赖于许多先前计算出的局部变量,如果您尝试将每个变量作为单独的参数进行传递,则会造成混乱。
hamstergene 2014年

358

只需使用其他功能,即可使用第二个版本:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

使用深度嵌套的ifs(您的第一个变体)或希望突破“一部分功能”通常意味着您确实需要额外的功能。


51
+1终于有人发布了合理的答案。我认为这是最正确,安全和易读的方式。
隆丁2014年

31
+1这很好地说明了“单一责任原则”。功能foo通过一系列相关条件和动作发挥作用。功能bar与决策完全分开。如果我们看到条件和操作的详细信息,可能会发现它foo仍然做得太多,但是现在这是一个很好的解决方案。
GraniteRobert

13
缺点是C没有嵌套函数,因此如果要使用bar您的变量需要这3个步骤,则必须手动将其foo作为参数传递。如果是这样,并且foo只调用一次,那我会倾向于使用goto版本,以避免定义两个紧密耦合的函数,这些函数最终将不会非常重用。
hugomg 2014年

7
不确定C的短路语法,但在C#中,foo()可以写为if (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

6
@ user1598390 Goto一直被使用,尤其是在需要取消大量设置代码的系统编程中。
Scotty Bauer 2014年

166

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

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通过将身体包裹成一个环并从中破坏来解决使用问题,但是两种方法实际上都可以完成相同的事情。goto如果仅executeStepA()在成功后才需要进行其他清理,则该方法更好:

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;
}

在这种情况下,使用循环方法将导致两个级别的循环。


108
+1。很多人看到a goto并立即认为“这是可怕的代码”,但是它确实有其有效的用法。
Graeme Perrow 2014年

46
从维护的角度来看,这实际上是非常混乱的代码。特别是在有多个标签的情况下,代码也变得更难阅读。有更优雅的方法可以实现:使用函数。
伦丁2014年

29
-1我看到了goto,我什至不必考虑看到这是糟糕的代码。我曾经不得不保持这种状态,这很讨厌。OP在问题的末尾提出了一种合理的C语言替代方案,我将其包括在答案中。
干杯和健康。-Alf

56
有限,独立的goto使用没有什么错。但是请注意,goto 一种入门药物,如果您不小心的话,有一天您会意识到其他人都没有用脚吃意大利面,并且在您想到“我可以解决这个问题”之后,您已经这样做了15年否则带有标签的梦bug般的错误……”
蒂姆·波斯特

66
我已经用这种风格写了十万行极其清晰,易于维护的代码。要理解的两个关键事项是(1)纪律!为每个函数的布局建立明确的准则,并且仅在极端需要时才违反它,并且(2)理解我们在这里所做的是使用没有它们的语言来模拟异常throw在许多方面都比goto因为在本地情况下throw甚至无法确定最终结果还不清楚而糟糕!对goto样式的控制流使用与对异常相同的设计注意事项。
Eric Lippert 2014年

131

这是一种常见的情况,有许多常见的处理方法。这是我尝试得出的标准答案。如果我错过了任何事情,请发表评论,我将保持最新状态。

这是一支箭

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

用后卫拉平箭

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

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;
} 

这在客观上和数量上都更易于阅读,因为

  1. 给定逻辑块的{和}字符靠得更近
  2. 理解特定线条所需的精神背景量较小
  3. 与if条件相关联的整个逻辑更可能在一页上
  4. 大大减少了编码人员滚动页面/眼睛轨迹的需要

如何在末尾添加通用代码

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

  1. 它以某种错误的方式给人们带来了麻烦,例如,学会使用Pascal进行编码的人们已经知道一个函数=一个出口点。
  2. 它没有提供无论退出什么都会在退出时执行的代码部分,而这是当前的主题。

下面,我通过使用语言功能或完全避免了此问题,提供了一些解决此限制的选项。

选项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();
}

选项2.避免出现此问题:重组功能

您可以通过将代码分成两个函数来避免此问题。该解决方案的优势是可以使用任何语言,此外,它还可以降低循环复杂性,这是降低缺陷率并提高任何自动化单元测试特异性的行之有效的方法。

这是一个例子:

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.利用对象生命周期

另一种方法是利用对象生命周期。使用上下文对象携带您的参数(我们的朴素示例可能缺少的内容)并在完成后将其处理。

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时,将永远不会评估右侧。

当需要紧凑代码并且代码不太可能需要太多维护时(例如,您正在实现一种众所周知的算法),此技巧最有用。对于更通用的编码,此代码的结构太脆弱了;即使对逻辑进行很小的更改也可能会触发完全重写。


12
最后?C ++没有finally子句。在您认为需要finally子句的情况下,可以使用带有析构函数的对象。
Bill Door

1
编辑以适应上面的两个评论。
John Wu

2
使用简单的代码(例如,在我的示例中),嵌套模式可能更容易理解。使用现实世界的代码(可能跨越几页),保护图案在客观上更易于阅读,因为它将需要更少的滚动和更少的眼睛跟踪,例如,从{到}的平均距离更短。
John Wu

1
我看到了嵌套模式,该代码不再在1920 x 1080屏幕上可见代码...尝试找出如果第三次操作失败将执行哪些错误处理代码...我曾经用do {...} (0)相反,所以您不需要最后的休息(另一方面,当(true){...}允许“ continue”重新开始时
gnasher729 2014年

2
您的选项4实际上是C ++中的内存泄漏(忽略较小的语法错误)。在这种情况下,不用“ new”,只需说“ MyContext myContext;”即可。
Sumudu Fernando 2014年

60

做就是了

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

就这么简单。


由于进行了三项修改,每项修改均从根本上改变了问题(如果将修订版本计算为版本1,则进行了四次修改),因此我提供了要回答的代码示例:

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

executeThisFunctionInAnyCase();

14
我回答了这个问题(更为简洁),该问题的第一个版本得到了20票赞成,在经过《黑暗轨道》中的评论和编辑后,被蜥蜴人Bill删除。
干杯和健康。-Alf

1
@ Cheersandhth-Alf:我不敢相信它被国防部删除了。那太糟糕了。(+1)
顺磁牛角包

2
我为您的+1勇气!(3次很重要:D)。
c

对于新手程序员来说,了解具有多个布尔“和”的命令执行,在不同语言中的不同之处等非常重要。这个答案很好地指出了这一点。但这只是普通商业编程中的“入门者”。这不简单,不可维护,不可修改。当然,如果您只是为自己编写一些快速的“牛仔代码”,请执行此操作。但是它只是“与当今实践的日常软件工程没有任何联系”。为可笑“编辑混淆”顺便说一句抱歉让你在这里忍受,@cheers :)
Fattie

1
@JoeBlow:我和Alf在一起-我发现&&列表更加清晰。我通常将条件分成几行,并// explanation在每行后添加尾随...。最后,要看的代码要少得多,一旦您了解了如何&&工作,就不会再费劲。我的印象是,大多数专业的C ++程序员都会对此很熟悉,但是正如您所说的,在不同的行业/项目中,重点和经验是不同的。
Tony Delroy 2014年

35

实际上,在C ++中有一种延迟动作的方法:利用对象的析构函数。

假设您有权访问C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

然后使用该实用程序:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo

67
这对我来说完全是个困惑。我真的不明白为什么这么多的C ++程序员喜欢通过尽可能多的语言功能来解决问题,直到每个人都忘记了您实际上要解决的问题:他们不再关心,因为他们现在对使用所有这些外来语言功能非常感兴趣。从那以后,您可以让自己几天和几周的时间忙于编写元代码,然后维护元代码,然后编写元代码来处理元代码。
隆丁2014年

24
@Lundin:好吧,我不明白如何对脆性代码感到满意,因为一旦引入了早期的continue / break / return或引发了异常,这些脆性代码就会立即崩溃。另一方面,该解决方案在将来进行维护时具有弹性,并且仅依赖于析构函数在展开期间执行的事实,这是C ++最重要的功能之一。至少可以这样说,限定这是一个奇特的选择,而为所有标准容器提供支持的基本原理很有趣。
Matthieu M.

7
@Lundin:Matthieu的代码的好处是executeThisFunctionInAnyCase();即使foo();抛出异常也会执行。编写异常安全代码时,最好将所有此类清除函数放在析构函数中。
布莱恩(Brian

6
@Brian然后不要在中抛出任何异常foo()。如果您这样做,请抓住它。问题解决了。通过修正错误来修正错误,而不是通过编写替代方法来修正。
伦丁2014年

18
@Lundin:Defer该类是可重用的小代码段,可让您以异常安全的方式进行任何块末尾清理。它通常被称为范围守卫。是的,可以使用其他更手动的方式来表示对作用域保护的使用,就像任何for循环都可以表示为一个块和一个while循环一样,如果需要,它们可以依次用if和表示,并且goto可以用汇编语言表示,或对于那些真正的大师,通过特殊的咕gr声和圣歌的蝴蝶效应,通过宇宙射线改变内存中的位。但是,为什么这样做。
干杯和健康。-Alf 2014年

34

有一种很好的技术,它不需要带有return语句的附加包装函数(由Itjax指定的方法)。它利用了do while(0)伪循环。在while (0)确保它其实并不是一个循环,但只执行一次。但是,循环语法允许使用break语句。

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}

11
在我看来,使用具有多个返回值的函数是相似的,但更具可读性。
伦丁2014年

4
是的,绝对是更易读的...但是从效率的角度来看,可以使用do {} while(0)构造避免额外的函数调用(参数传递和返回)开销。
Debasis 2014年

5
您仍然可以自由使用该功能inline。无论如何,这是一个很好的技术,因为它不仅可以解决这个问题。
Ruslan 2014年

2
@Lundin您必须考虑代码局部性,将代码散布到太多地方也有问题。
API-Beast

3
以我的经验,这是一个非常不寻常的习惯用法。我要花点时间弄清楚到底发生了什么,这在我查看代码时是一个不好的信号。鉴于它似乎没有其他优势,因此没有其他优势,因此更无法接受。
科迪·格雷

19

您也可以这样做:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

这样,您的线性增长量就最小,每次通话+1行,并且易于维护。


编辑:(感谢@Unda)不是一个大粉丝,因为您失去了可见性IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

1
这是关于push_back()中意外的函数调用,但无论如何您都已修复它:)
Quentin 2014年

4
我很想知道为什么这引起了人们的反对。假设执行步骤确实是对称的(看起来好像是对称的),那么它是干净且可维护的。
ClickRick 2014年

1
尽管可以说这看起来更干净。对于人们而言,这可能更难理解,对于编译器而言,绝对更难理解!
Roy T.

1
将函数更多地视为数据通常是一个好主意-完成后,您还会注意到后续的重构。更好的是,您要处理的每个部分都是对象引用,而不仅仅是函数引用-这将使您有更多的能力来改进代码。
比尔K

3
对于一个微不足道的案例,它略有过度设计,但是该技术绝对具有其他人所没有的好功能:您可以更改执行顺序和运行时调用的函数数,这很不错:)
Xocoatzin

18

这行得通吗?我认为这与您的代码等效。

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();

3
ok当使用像这样的相同变量时,我通常会调用该变量。
Macke 2014年

1
我很想知道为什么要投票。这是怎么了
ABCplus 2014年

1
比较您的答案与短路方法的圈复杂度。
AlphaGoku

14

假设所需的代码是我当前看到的:

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

我要说的是,正确的方法是最简单的阅读方法,并且最容易维护,因此缩进级别较少,这是(当前)问题的既定目的。

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

这样就避免了对s,异常,伪循环或其他困难构造的任何需要,并且可以轻松完成手头的简单工作。gotowhile


使用循环时,通常不需要使用额外的“标志”变量就可以使用returnbreak跳出循环。在这种情况下,使用goto同样会毫无用处-请记住,您要用额外的goto复杂度来换取额外的可变变量复杂度。
hugomg 2014年

2
@hugomg变量在原始问题中。这里没有多余的复杂性。对这个问题做出了一些假设(例如,在已编辑的代码中需要使用变量),因此将其保留下来。如果不需要它们,则可以简化代码,但是鉴于问题的不完整性质,没有其他可以有效地做出的假设。
ClickRick 2014年

特别有用的方法。可供自称使用newbie,它提供了一个没有缺点的更干净的解决方案。我注意到它也不不依赖于steps具有相同的签名,甚至不依赖于功能而不是块。我可以看到,即使在更复杂的方法有效的情况下,也可以将其用作首过重构。
基思2014年

12

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

也许不是最好的解决方案,但是您可以将语句放入do .. while (0)循环中,然后使用break语句代替 return


2
不是我否决了它,但这是对某种循环构造的滥用,这种循环构造的作用是当前想要的但不可避免地会导致痛苦。可能是下一个必须在您继续进行另一个项目后两年内对其进行维护的开发人员。
ClickRick

3
do .. while (0)用于宏定义的@ClickRick 也会滥用循环,但可以接受。
哇,2014年

1
也许可以,但是有更清洁的方法可以实现它。
ClickRick

4
@ClickRick我的答案的唯一目的是回答可以以某种方式使用break语句,并且答案是肯定的,我的答案中的第一句话表明这可能不是解决方案。
哇,2014年

2
这个答案应该只是评论
msmucker0527 2014年

12

您可以将所有if条件(按需要的格式)放在自己的函数中,然后在返回时执行该executeThisFunctionInAnyCase()函数。

从OP中的基本示例中,可以将条件测试和执行本身分开;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

然后这样称呼;

InitialSteps();
executeThisFunctionInAnyCase();

如果C ++ 11 lambda可用(OP中没有C ++ 11标记,但它们仍然是一个选项),那么我们可以放弃单独的函数,并将其包装为lambda。

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();

10

如果您不喜欢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();

1
if 您不喜欢goto而 && 您不喜欢{}而(0) && 您喜欢C ++ ...对不起,无法抗拒,但是最后一个条件失败了,因为该问题被标记为c以及c ++
ClickRick

@ClickRick总是很难取悦所有人。在看来,没有C / C ++之类的东西,您通常在其中任何一个中编写代码,而对另一个的使用却不满意。
Alex 2014年

9

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

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

我确定还有其他方法可以写一长串if / else,但是我怀疑它们会改变什么,除了链对您来说看起来很漂亮(但是,情人眼中的美丽仍然适用于代码)也是:-))。您应该担心诸如此类的事情(在我有一个新状况的6个月内,如果我不记得任何有关此代码的信息,我将能够轻松地添加它吗?或者如果链发生变化,如何快速且无错误地发生?我会实施吗?


9

你只是这样做。

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

这是100的99倍,这是唯一的方法。

永远不要尝试在计算机代码中做一些“棘手的”事情。


顺便说一下,我很确定以下是您想到实际解决方案...

继续语句是算法编程的关键。(就像goto语句在算法编程中至关重要。)

在许多编程语言中,您可以执行以下操作:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(首先请注意:像该示例这样的裸露块是编写漂亮代码的关键和重要部分,尤其是在处理“算法”编程时。)

再说一遍,这就是您的头脑,对吗?那是编写它的美丽方式,因此您具有良好的直觉。

但是,可悲的是,在当前版本的Objective-C中(还有-我不了解Swift,对不起),它有一个令人眼花feature乱的功能,它可以检查封闭的块是否为循环。

在此处输入图片说明

这是您如何解决的...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

所以不要忘了..

做{} while(false);

只是意味着“一次执行此阻止”。

即,写作do{}while(false);和简单写作之间完全没有区别{}

现在,它可以按您的要求完美运行...这是输出...

在此处输入图片说明

因此,这可能就是您脑海中看到的算法。您应该始终尝试写下您的想法。(特别是如果你不清醒的话,因为那是漂亮的时候出来的!:))

在经常发生这种情况的“算法”项目中,在objective-c中,我们总是有一个类似于...的宏。

#define RUNONCE while(false)

...所以您可以执行此操作...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

有两点:

a,即使objective-c愚蠢地检查continue语句所在的块的类型,也很难“与之抗争”。因此,这是一个艰难的决定。

b,在示例中,您应该缩进那个问题吗?我因为诸如此类的问题而失眠,所以我不建议。

希望能帮助到你。



您是否将赏金用于获得更多代表?:)
TMS

除了将所有这些注释放在中if,您还可以使用更具描述性的函数名称,并将注释放入函数中。
Thomas Ahle 2014年

uck 我将采用1行解决方案来简洁地阅读短路评估(这种评估已经使用了20多年的语言,并且众所周知)在每天的混乱之中。我认为我们都可以同意我们很高兴不与彼此合作。
M2tM 2014年

8

如果执行函数失败,则使它们抛出异常,而不是返回false。然后,您的调用代码可能如下所示:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

当然,我假设在您的原始示例中,如果在步骤内部发生错误,执行步骤将仅返回false?


3
使用异常控制流通常被认为是不良做法和有臭味的代码
user902383 2014年

8

已经有很多不错的答案,但是其中大多数似乎都在某些灵活性(当然很少)之间进行了权衡。不需要此折衷的常见方法是添加状态/持续变量。当然,价格是一项额外的价值,可用于跟踪:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;

为什么ok &= conditionX;而不是简单地ok = conditionX;
ABCplus 2014年

@ user3253359在很多情况下,可以的。这是一个概念演示。在工作代码中,我们将尝试尽可能地简化它
blgt 2014年

+1 是问题中规定的在c中有效的少数几个干净且可维护的答案之一。
ClickRick

6

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

将值检查重构为异常不一定是一个好方法,因为要消除异常会产生大量开销。
John Wu

4
-1像这样在C ++中为正常流程使用异常是不好的编程习惯。在C ++中,应为特殊情况保留例外。
杰克·艾德利2014年

1
在问题文本中(我强调):“仅当前一个成功时,才应执行函数executeStepX ”换句话说,返回值用于指示失败。也就是说,这是错误处理(并且希望失败例外的)。错误处理正是发明异常的原因。
celtschk 2014年

1
不。首先,创建例外是为了允许错误传播,而不是错误处理;其次,“只有在前一个成功的情况下,才应执行函数executeStepX”。并不意味着先前函数返回的布尔值false表示明显的例外/错误情况。因此,您的陈述是不合逻辑的。错误处理和流清除可以通过许多不同的方法来实现,例外是一种允许错误传播就地错误处理的工具,并且在这方面表现出色。

6

如果您的代码和示例一样简单,并且您的语言支持短路评估,则可以尝试以下操作:

StepA() && StepB() && StepC() && StepD();
DoAlways();

如果将参数传递给函数并返回其他结果,以使您的代码无法以以前的方式编写,则许多其他答案将更适合该问题。


实际上,我编辑了问题以更好地解释该主题,但是由于没有使大多数答案无效而被拒绝。:\
ABCplus

我是SO的新用户,也是新手程序员。那么有2个问题:是否有可能因为这个问题而将另一个问题(如您所说的问题)标记为重复?另一点是:新手SO用户/程序员如何才能在所有这些问题之间选择最佳答案(我想这差不多)。
ABCplus

6

对于C ++ 11及更高版本,一种不错的方法可能是实现类似于D的scope(exit)机制的范围退出系统。

实现它的一种可能方法是使用C ++ 11 lambda和一些帮助程序宏:

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()可以直接使用。


不需要宏就可以完成这项工作。而且[=]通常是错的作用域拉姆达。
Yakk-Adam Nevraumont 2014年

是的,宏仅用于装饰,可以丢弃。但是您难道不是说按价值捕获是最安全的“通用”方法吗?
glampert 2014年

1
否:如果您的lambda不会在创建lambda的当前范围之外继续存在,请使用[&]:它是安全的,并且几乎没有什么惊奇。仅当lambda(或副本)的生存时间比声明时的作用范围长时,才可以按值捕获…
Yakk-Adam Nevraumont 2014年

是的,那很有道理。我会改变它。谢谢!
glampert 2014年

6

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

如果您所有的函数都具有相同的签名,那么您可以使用这种方式(对于C语言):

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

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

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

对于干净的C ++解决方案,您应该创建一个接口类,其中包含一个 execute方法并将步骤包装在对象中。
然后,以上解决方案将如下所示:

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

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

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

executeThisFunctionInAnyCase();

5

假设您不需要单独的条件变量,则将测试取反并使用else-falthrough作为“ ok”路径,将可以使您获得更多垂直的if / else语句集:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

忽略失败的变量会使代码使IMO变得晦涩难懂。

声明内部变量很好,不用担心= vs ==。

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

这是模糊的,但紧凑的:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();

5

这看起来像状态机,它很方便,因为您可以轻松地使用state-pattern实现它。

在Java中,它看起来像这样:

interface StepState{
public StepState performStep();
}

一个实现将如下工作:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

等等。然后,您可以将big if条件替换为:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();

5

正如Rommik提到的那样,您可以为此应用设计模式,但是由于要链接调用,因此我将使用Decorator模式而不是Strategy。如果代码很简单,那么我将采用结构合理的答案之一来防止嵌套。但是,如果它很复杂或需要动态链接,则装饰器模式是一个不错的选择。这是一个yUML类图

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


这比Jefffrey的答案有什么好处?
戴森2014年

当需求改变时,这种方法更容易适应变化,而无需大量领域知识,这种方法更易于管理。尤其是当您考虑嵌套ifs的某些部分可以多长和多长时。一切都会变得非常脆弱,因此使用起来的风险很高。不要误会我的意思,某些优化方案可能会导致您将其删除并返回到ifs,但是99%的时间都可以。但是关键是,当您达到那个级别时,您就不必在乎可维护性,而需要性能。
约翰·尼古拉斯

4

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

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

我使用宏while(false)来使其成为do { } once;通用模式:

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

这种模式相对易于阅读,并且允许使用将被适当破坏的对象,并且还避免了多次返回,从而使步进和调试更加容易。


4

为了改善Mathieu的C ++ 11答案并避免由于使用引起的运行时成本std::function,我建议使用以下代码

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

这个简单的模板类将接受可以在没有任何参数的情况下调用的任何函子,并且无需任何动态内存分配即可这样做,因此可以更好地符合C ++的抽象目标,而不会产生不必要的开销。附加功能模板在那里可以简化模板参数推导的使用(不适用于类模板参数)

用法示例:

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

正如Mathieu的回答一样,此解决方案是完全例外安全的,并且executeThisFunctionInAnyCase在所有情况下都将被调用。如果executeThisFunctionInAnyCase自身抛出,析构函数将被隐式标记noexcept,因此std::terminate将发出对的调用,而不是导致在堆栈展开期间抛出异常。


+1我一直在寻找这个答案,所以我不必发布它。您应该functordeferred'd'构造函数中完善前进,而无需强制使用move
Yakk-Adam Nevraumont 2014年

@Yakk将构造函数更改为转发构造函数

3

似乎您想在一个块中进行所有通话。正如其他人所建议的那样,您应该使用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();

幸运的是,调试器会为您找到它们。如果您正在调试而不是单步执行代码,那么您做错了。
科迪·格雷

我不明白您的意思,也为什么我不能使用单步执行?
AxFab 2014年

3

因为您在执行之间也有[...代码块...],所以我猜您有内存分配或对象初始化。这样,您必须关心清除在退出时已初始化的所有内容,如果遇到问题并且任何函数都将返回false,也必须对其进行清理。

在这种情况下,根据我的经验(当我使用CryptoAPI时),最好的做法是创建小类,在构造函数中初始化数据,在析构函数中取消初始化数据。每个下一个功能类必须是上一个功能类的子级。如果出现问题-抛出异常。

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

然后在您的代码中只需调用:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

我猜这是最好的解决方案,如果ConditionX的每次调用都初始化一些东西,分配内存等。最好确保一切都会被清除。


3

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

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

如果编写这样的代码,您将朝着错误的方向前进。我不会将拥有这样的代码看作是“问题”,而是拥有如此凌乱的“架构”。

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


我认为这个想法不能取代每个if链。无论如何,在很多情况下,这是一个非常好的方法!
2014年

3

另一种方法- 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结束时进行检查(不过,可以跳过)。


嘿Zaffy-这个答案对do {} while(false)方法有很大的解释。 stackoverflow.com/a/24588605/294884 另外两个答案也提到了它。
Fattie 2014年

在这种情况下使用CONTINUE还是BREAK会更优雅,这是一个有趣的问题!
Fattie 2014年

嘿@JoeBlow我看到了所有答案……只是想展示而不是谈论它:)
Zaffy 2014年

我在这里的第一个回答是:“没有人提到此事……”,然后有人友善地指出这是第二大回答:)
Fattie 2014年

@JoeBlow嗯,是的。我会设法解决的。我觉得... xD无论如何,谢谢您,下次我将支付更多的
比赛
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.