如何避免"如果"链?

ABC*_*lus 262 c c++ if-statement control-flow

假设我有这个伪代码:

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

executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

executeStepX当且仅当前一个成功时才应执行函数.无论如何,executeThisFunctionInAnyCase应该在最后调用该函数.我是编程的新手,对于这个非常基本的问题感到抱歉:有没有办法(例如在C/C++中)避免长if链产生那种"代码金字塔",代价是代码易读性?

我知道如果我们可以跳过executeThisFunctionInAnyCase函数调用,代码可以简化为:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
Run Code Online (Sandbox Code Playgroud)

但约束是executeThisFunctionInAnyCase函数调用.该break声明是否可以某种方式使用?

Sho*_*hoe 485

你可以使用&&(逻辑AND):

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

这将满足您的两个要求:

  • executeStep<X>()应仅评估前一个是否成功(这称为短路评估)
  • executeThisFunctionInAnyCase() 将在任何情况下执行

  • @RobAu:这不是利用一些模糊的语法技巧的黑客,它几乎在每种编程语言中都是高度惯用的,甚至是不可能的标准实践. (61认同)
  • @RobAu:那么初级程序员最终会看到依赖于快捷评估的代码并且可能促使他们研究这个主题,这反过来会帮助他们最终成为高级程序员.显然是一个双赢:体面的代码和有人从阅读中学到了一些东西. (58认同)
  • 此解决方案仅适用于条件实际上是简单函数调用的情况.在实际代码中,这些条件可能是2-5行(并且它们本身是许多其他`&&'和`||`的组合)所以你不可能想要将它们加入到单个`if`语句中而不会破坏可读性.并且将这些条件移动到外部函数并不总是容易的,因为它们可能依赖于许多先前计算的局部变量,如果您尝试将每个变量作为单独的参数传递,则会产生可怕的混乱. (38认同)
  • 这在语义上是正确的(实际上,我们希望所有条件都是真的)以及非常好的编程技术(短路评估).此外,这可以用于任何复杂的情况,其中创建函数会弄乱代码. (29认同)
  • 这应该是最好的答案 (24认同)
  • 虽然这个答案为了简洁而得分为A,但它的可维护性和可扩展性也得分为F. 如果你需要在三个函数调用周围添加工作变量,日志记录,甚至是最近的一点逻辑,这个模式会立即中断. (20认同)
  • @JoeBlow也许你在寻找程序员时遇到了麻烦,因为没有人想为那些认为逻辑短路并且是'聪明屁股的棘手代码'的人工作? (16认同)
  • @modiX VB.NET确实有短路评估,如果你使用AndAlso或OrElse而不仅仅是And和Or. (11认同)
  • 那么VB.NET不是一种编程语言,而是一种坏习惯.另外@Aufziehvogel &&不是'副作用'.这是它必须表现的方式.它实际上建立在非常简单的东西上:数学公式.您从左到右进行评估,如果遇到括号,则计算括号内的所有内容,并仅在语句中使用结果. (9认同)
  • 这很容易成为答案列表中最清晰,最干净的解决方案.非常好. (7认同)
  • 当有许多局部变量需要与`executeStep`方法进行通信时,该技术不能很好地工作.它也传播代码是一种不合逻辑的方式. (5认同)
  • 不过,这可能是调试的噩梦.我们失败了什么线?在第27行,你说?太糟糕了,第27行有八个friggin函数可能爆炸...... (5认同)
  • @corsiKa`if(executeStepA()\n && executeStepB()\n && executeStepC()){` (5认同)
  • 实际上,从软件工程师(或建筑师?)的角度来看,这种技术被认为是非常邪恶的(我不认为自己是这样的,但是作为每个人我都有一个关于它作为大学的讲座).根据这个讲座,应该避免if语句中的各种副作用,因为它可能导致错误.当然,软件架构师通常不喜欢C和C++. (3认同)
  • 在返回do log之前添加函数中的日志记录("<function name>:"+ result); 这是手头问题的最佳答案.如果你正在寻找更复杂的多行ifs,请问一个单独的问题,我会告诉你让函数返回false,否则"return funcB();" 等等. (3认同)
  • 不,这不是最好的答案,你对日志记录的回答很差(如果你调用的函数在第三方库中怎么办?)并且你没有解决局部变量或其他逻辑的问题.虽然快速,简短和常见,但这个答案指向的是一种在现实世界的代码中实际上非常脆弱的技术, (3认同)
  • @JohnWu,你只需在(局部)函数中包含所有进入'条件'的代码.在'if语句'中进行记录是非常糟糕的. (3认同)
  • @JohnWu是的,你可以很容易地超过这个成语的有用限制.然而,这甚至不具有远程相关性,因为海报询问的情况是否在这些限制之内.当脚手架开始吹眼球测试时(你能看到控制流和副作用吗?没有?修复该死的代码!)还有其他方法 - 但它们是对不同问题的良好答案. (3认同)
  • 这很有趣:)但是我很抱歉,它不简单,可维护,可记录,可更改. (2认同)
  • @JoeBlow - 短路肯定会被滥用(并且一个C++开发人员重载`operator &&`应该被隔离),但我发现克制使用,例如用于探测可选指针,简洁明了,例如"if(bounding_box && bounding_box-) > width> val){...}" (2认同)

ltj*_*jax 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();
}
Run Code Online (Sandbox Code Playgroud)

使用深度嵌套的ifs(你的第一个变体)或者想要打破"函数的一部分"通常意味着你需要一个额外的功能.

  • +1终于有人发布了一个明智的答案.在我看来,这是最正确,安全和可读的方式. (51认同)
  • +1这是"单一责任原则"的一个很好的例证.函数`foo`通过一系列相关的条件和动作.函数`bar`与决策完全分开.如果我们看到条件和动作的细节,可能会发现`foo`仍然做得太多,但是现在这是一个很好的解决方案. (31认同)
  • 缺点是C没有嵌套函数,所以如果需要使用`bar`中的变量这三个步骤,你将不得不手动将它们作为参数传递给`foo`.如果是这种情况,如果`foo`只被调用一次我会错误地使用goto版本来避免定义两个紧密耦合的函数,最终不会重复使用. (13认同)
  • 不确定C的短路语法,但在C#中,foo()可以写成`if(!executeStepA()||!executeStepB()||!executeStepC())return` (7认同)
  • @ user1598390 Goto是一直使用的,特别是在需要解开大量设置代码的系统编程时. (6认同)
  • 你应该只为你的程序中的事实上单独的功能制作方法,而不仅仅是因为你非常喜欢你的`return`语句.我肯定会通过`&&`运算符来推荐短路评估,因为它可以适用于任何地方而无需无意义的方法. (2认同)
  • 这里最好的是简单.分析这段代码几乎没有什么想法; 我可以很容易地看到什么是预期的,将会发生什么.+1 (2认同)
  • 我宁愿在底部找到Jefffrey的答案`if(executeStepA()&& executeStepB()&& executeStepC())`看起来更聪明.跳转并调用另一个func比这个慢. (2认同)
  • 明确的boolans在这里毫无用处.只需执行`if(!executeStepA())return`.它会更清洁. (2认同)
  • @HotLicks我认为在那种情况下你的方法太长了.被跳过的代码将是"executeThisFunctionInAnyCase",这个答案很好地解决了. (2认同)

cma*_*ter 165

老派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;
}
Run Code Online (Sandbox Code Playgroud)

有些人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;
}
Run Code Online (Sandbox Code Playgroud)

使用循环方法,在这种情况下最终会有两级循环.

  • +1.很多人看到`goto`并立即想到"这是一个可怕的代码"但它确实有其有效的用途. (108认同)
  • 我已经用这种风格写了十万行非常清晰,易于维护的代码.要理解的两个关键事项是(1)纪律!为每个功能的布局建立一个明确的指导方针,并且只在极端需要时违反它,并且(2)理解我们在这里做的是*用没有它们的语言模拟异常*.`throw`在很多方面比`goto'更糟糕,因为`throw`它甚至不清楚*来自当地环境*你最终会结束!对goto样式的控制流使用与异常相同的设计注意事项. (66认同)
  • 这种有限的,自足的goto使用没有任何问题.但要注意,如果你不小心,你有一天会意识到没有人用他们的脚吃意大利面,而且你已经做了15年了,你开始认为"我可以解决这个问题"否则带有标签的噩梦......" (56认同)
  • 从维护的角度来看,这实际上是非常混乱的代码.特别是当存在多个标签时,代码也变得难以阅读.有更优雅的方法来实现这一点:使用功能. (46认同)
  • -1我看到`goto`,我甚至不必考虑这是可怕的代码.我曾经不得不维持这样,这是非常讨厌的.OP在问题的最后提出了一个合理的C语言替代方案,我在答案中包括了这一点. (29认同)
  • +1.我不是单点进入/单点退出的忠实粉丝,我几乎没有使用过.但是当绝对需要清理时,goto可以是实现这一目标的最清晰,最干净的方式. (8认同)
  • 作为一个经验法则,像这一个向前跳跃的那些并不像向后跳跃那样糟糕. (7认同)
  • 我倾向于将"goto"比作一堆炸药.有很多程序员如此害怕它们,当他们在他们选择的路径中找到一块巨石时,他们会在它周围和周围构建复杂的支架,因为代码效率和可读性,它真的更有意义.把它炸掉 在我看来,这源于CS教授,他们懒得教他们的学生如何正确使用goto,所以简单地说goto自动意味着"坏代码". (7认同)
  • 我们没有从[Apple的goto fail](https://www.imperialviolet.org/2014/02/22/applebug.html)中学到任何东西吗?至少放括号以避免这种情况! (6认同)
  • @ Cheersandhth.-Alf:我更喜欢蓝色的自行车棚. (5认同)
  • -1 - 第一个例子很干净,但是带有两个标签的第二个例子已经开始变得混乱,这也是为什么基于goto的代码无法扩展的一个很好的例子.如果您使用的是gotos,然后发现您需要具有两个或更多标签的复杂退出条件,那么可能是重构的时候了. (5认同)
  • @Lundin有趣的是,您可以将Linux样式指南称为业余级文档.刚刚看了一下这个文档的git-blame,结果看到关于集中式函数退出的部分是由Linus Torvalds本人编写的.是的,可能会有争议.他说的很多内容都是有争议的.但我会毫不犹豫地称之为"业余级别".毕竟,我们正在讨论最大和最成功的开源项目基础上的文档(是的,在基础上:风格指南是任何项目中最有影响力的文档之一,如果它被尊重). (5认同)
  • @mgarciaisaia请注意,我将`goto`*放在与`if`相同的行*上.这正是为了防止像'goto fail`这样的错误.危险在于以一种允许任何一个被意外删除的方式将身体状况分开.无论您使用大括号还是oneline if语句来防止此类错误都是风格问题.另请注意,如果按照许多编码标准的要求在自己的行上编写左大括号,即使是大括号也不会使您不会意外地删除条件. (4认同)
  • 听起来更像是Java的"重大问题".这正是我在使用C代码或C++代码时匆忙使用的解决方案.(否则,我会在C++中使用RAII.)没有其他方法可以编写COM代码并保持所有清理可读性. (4认同)
  • 顺便说一句[Linux风格指南](https://www.kernel.org/doc/Documentation/CodingStyle)是一个业余级别的文档,由一个显然没有写过这类东西经验的人写的.此外,从一般的C canon观点来看,该文件的大部分内容都存在争议.因此,不要在任何严肃的辩论中引用它,好像它是某种C权威.如果您想引用编码标准作为某种实践的参考,请参考专业的,广泛认可的编码标准,如MISRA-C或CERT C.(事实证明,CERT C实际上支持您的答案) (2认同)
  • -1表示第二个例子.虽然我接受第一个,但第二个是太乱了. (2认同)
  • 如果我没错,这个代码是不是与在iOS上创建SSL大问题的代码差不多?http://nakedsecurity.sophos.com/2014/02/24/anatomy-of-a-goto-fail-apples-ssl-bug-explained-plus-an-unofficial-patch/会造成大混乱...... (2认同)

Joh*_* Wu 129

这是一种常见的情况,有许多常见的方法可以解决它.这是我对规范答案的尝试.如果我遗漏了任何内容,请发表评论,我会及时更新这篇文章.

这是一个箭头

你在讨论什么被称为箭头反模式.它被称为箭头,因为嵌套ifs链形成代码块,它们向右和向后扩展得越来越远,形成一个"指向"代码编辑器窗格右侧的可视箭头.

用卫兵展平箭头

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

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}
Run Code Online (Sandbox Code Playgroud)

......你用......

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above
Run Code Online (Sandbox Code Playgroud)

当有一长串防护装置时,这会使代码变得相当扁平,因为所有防护装置一直显示在左边,而你的ifs没有嵌套.此外,您可以直观地将逻辑条件与其相关错误配对,这样可以更容易地判断出发生了什么:

箭头:

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;
}
Run Code Online (Sandbox Code Playgroud)

守护:

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;
} 
Run Code Online (Sandbox Code Playgroud)

这是客观和量化的更容易阅读,因为

  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();
}
Run Code Online (Sandbox Code Playgroud)

选项2.避免问题:重组您的功能

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

这是一个例子:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}
Run Code Online (Sandbox Code Playgroud)

选项3.语言技巧:使用假循环

我看到的另一个常见技巧是使用while(true)和break,如其他答案所示.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();
Run Code Online (Sandbox Code Playgroud)

虽然这不像使用那么"诚实" 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
}
Run Code Online (Sandbox Code Playgroud)

注意:确保您了解所选语言的对象生命周期.为此需要某种确定性的垃圾收集,即你必须知道何时调用析构函数.在某些语言中,您将需要使用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
Run Code Online (Sandbox Code Playgroud)

再次,确保您了解对象的生命周期.

选项5.语言技巧:使用短路评估

另一种技术是利用短路评估.

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();
Run Code Online (Sandbox Code Playgroud)

此解决方案利用了&&运算符的工作方式.当&&的左侧评估为false时,永远不会评估右侧.

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

  • 最后?C++没有finally子句.在您认为需要finally子句的情况下,使用具有析构函数的对象. (12认同)
  • 使用简单的代码(例如在我的示例中)嵌套模式可能更容易理解.使用真实世界代码(可以跨越几页),保护模式在客观上更容易阅读,因为它需要更少的滚动和更少的眼动追踪,例如{到}的平均距离更短. (2认同)
  • 我已经看到代码在 1920 x 1080 屏幕上不再可见的嵌套模式......尝试找出如果第三个操作失败将执行哪些错误处理代码......我已经使用了 do { ... } while (0) 代替所以你不需要最后的休息(另一方面, while (true) { ... } 允许“继续”重新开始。 (2认同)
  • 您的选项4实际上是C++中的内存泄漏(忽略次要语法错误).在这种情况下,不会使用"new",只需说"MyContext myContext;". (2认同)

Che*_*Alf 60

做就是了

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

就这么简单.


由于三个编辑每个都从根本上改变了问题(如果将修订版本重新计算为版本#1,则有四个),我将包含我正在回答的代码示例:

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

executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

  • 我回答了这个问题(更简洁地回答了问题的第一个版本),在经过Orbit Races on Orbit的一些评论和编辑后,它被Bill the Lizard删除之前得到了20个upvotes. (14认同)
  • 我的+1给你的勇气!(3次确实很重要:D). (2认同)

Mat*_* M. 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
Run Code Online (Sandbox Code Playgroud)

然后使用该实用程序:

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

    // ...

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

    // ...

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

    // ...

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

    // ...

    return 4;
} // foo
Run Code Online (Sandbox Code Playgroud)

  • 这对我来说只是完全混淆了.我真的不明白为什么这么多C++程序员喜欢通过尽可能多地抛出语言功能来解决问题,直到每个人都忘记了你实际解决的问题为止:他们不再关心,因为他们是现在对使用所有这些外来语言功能感兴趣.从那以后,您可以让自己忙碌几天和几周编写元代码,然后维护元代码,然后编写用于处理元代码的元代码. (67认同)
  • @Lundin:嗯,我不明白如果一个早期的继续/中断/返回被引入或抛出异常就会破坏脆弱的代码.另一方面,这种解决方案在面向未来的维护时具有弹性,并且仅依赖于在展开期间执行析构函数这一事实,这是C++最重要的特性之一.鉴于*异国情调*的限定,虽然它是为所有标准容器提供动力的基本原则,但至少可以说是有趣的. (24认同)
  • @Lundin:`Defer`类是一个*可重用的*一小段代码,它允许你以异常安全的方式进行任何块结束清理.它通常被称为**范围防守**.是的,任何范围保护的使用都可以用其他更多的手动方式表达,就像任何`for`循环可以表示为一个块和一个`while`循环一样,它又可以用`if`和`goto`表示. ,如果你愿意,可以用汇编语言表达,或者对于那些真正的主人来说,通过由特殊短咕噜声和吟唱的蝴蝶效应引导的宇宙射线改变记忆中的位.但是,为什么这样做. (18认同)
  • @Lundin:Matthieu代码的好处是`executeThisFunctionInAnyCase();`即使`foo();`也会执行`抛出异常.在编写异常安全代码时,最好将所有这些清理函数放在析构函数中. (7认同)
  • @Brian然后不要在`foo()`中抛出任何异常.如果你这样做,抓住它.问题解决了.通过修复它们来修复错误,而不是通过编写解决方法. (6认同)
  • @Lundin:不幸的是,很难知道`foo()`是否会抛出异常,因为它可能会调用第三方编写的代码,可能会在以后编辑,并且由纪律严重的人编写.所以,你的回答实际上是在"把一个伪终结块之前发生的事情包装在try-catch中".但这使得代码非常难以维护.我用来编写带有异常的代码时遇到的最简单的指导原则基本上是,A)所有清理必须在析构函数中,B)异常不能逃避析构函数.您的提案会导致更复杂的指导方针. (5认同)
  • @Brian没有它不是非常困难,因为它是由_you_编写的函数.如果你决定包含一些你不知道它做什么的可疑代码,那么将它包装在它自己的函数中并将所有防御性编程测量放在该函数中.最重要的是你不应该这样做,因为任何正确的库应该记录它抛出的内容,甚至更好:它应该根本不使用任何例外.如果库是废话,请不要使用它.无论如何,我认为关于如何最好地处理写得不好的C++库的讨论是非常偏离主题的. (5认同)

Deb*_*sis 34

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

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}
Run Code Online (Sandbox Code Playgroud)

  • 在我看来,使用具有多个返回的函数是类似的,但更具可读性. (11认同)
  • 你仍然可以自由地使用"内联"功能.无论如何,这是一个很好的技术,因为它有助于解决这个问题. (5认同)
  • 是的,肯定它更具可读性......但是从效率的角度来看,使用do {} while(0)构造可以避免额外的函数调用(参数传递和返回)开销. (4认同)
  • 根据我的经验,这是一个非常不寻常的习语.我需要一段时间来弄清楚到底发生了什么,当我查看代码时,这是一个不好的迹象.鉴于它似乎没有其他更常见,因此更具可读性的方法,我无法签署它. (3认同)
  • @Lundin你必须考虑代码局部性,在太多地方传播代码也有问题. (2认同)
  • @Debasis使用此循环替换函数只是为了提高效率,预示着早熟优化.此外,即使编译器没有内联函数,很可能,开销很小.您不应该让这些微不足道的性能考虑因素影响您设计程序的方式. (2认同)

小智 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();
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您可以获得最小的线性增长大小,每次调用+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();
Run Code Online (Sandbox Code Playgroud)

  • 我很想知道为什么这会有一个downvote.假设执行步骤确实是对称的,就像它们看起来那样,那么它是干净且可维护的. (4认同)
  • 对于一个简单的案例略微过度设计,但这种技术肯定有一个很好的功能,其他人没有:你可以改变执行顺序和[运行时调用的函数数量],这很好:) (3认同)

sam*_*ris 18

这会有用吗?我认为这与您的代码相同.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

  • 我通常在使用相同的变量时调用变量`ok`. (3认同)

Cli*_*ick 14

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

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

我会说正确的方法,因为它是最简单的阅读和最容易维护,会有较少的缩进程度,这是(目前)问题的既定目的.

// 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();
Run Code Online (Sandbox Code Playgroud)

这避免了对s,异常,虚拟循环或其他困难构造的任何需要,并且只需简单地完成手头的简单工作.gotowhile

  • @hugomg变量在原始问题中.这里没有*额外的*复杂性.有关于该问题的假设(例如,在编辑的代码中需要变量),因此保留了它们.如果不需要它们,那么代码可以简化,但鉴于问题的不完整性,没有其他假设可以有效地进行. (2认同)

oua*_*uah 12

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

也许不是最好的解决方案,但你可以将你的语句放在一个do .. while (0)循环中,break而不是使用语句 return.

  • @ClickRick我回答的唯一目的是回答*可以以某种方式使用break语句*答案是肯定的,我的回答中的第一句话表明这可能不是使用的解决方案. (4认同)
  • @ClickRick使用`do .. while(0)`进行宏定义也是滥用循环但是它被认为是正常的. (3认同)
  • 不是我对它进行了贬低,但这会滥用循环结构,因为这种效果是目前所需的,但不可避免地会导致痛苦.可能是下一个开发人员在你转移到另一个项目之后需要在两年内维护它的人. (2认同)
  • 也许吧,但有更干净的方法来实现它。 (2认同)
  • 这个答案应该只是一个评论 (2认同)

Nia*_*all 12

您可以将所有if条件(根据需要进行格式化)放在自己的函数中,on返回执行executeThisFunctionInAnyCase()函数.

从OP中的基本示例,条件测试和执行可以这样拆分;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}
Run Code Online (Sandbox Code Playgroud)

然后这样称呼;

InitialSteps();
executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

如果C++ 11 lambdas可用(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();
Run Code Online (Sandbox Code Playgroud)


Ale*_*lex 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();
Run Code Online (Sandbox Code Playgroud)


Rom*_*Mik 9

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

通常,当您在代码中看到很多IF和其他内容时,可以实现策略设计模式(http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net)或者组合其他模式.

我确定有一些替代方法可以写一个if/else的长列表,但我怀疑他们会改变任何东西,除了链对你来说很漂亮(但是,旁观者眼中的美仍然适用于代码也是:-)).您应该关注这样的事情(在6个月内,当我遇到新情况并且我不记得有关此代码的任何内容时,我能够轻松添加它吗?或者如果链条发生变化,速度和无错误怎么办?我会实施吗)

  • 完全正确.OP需要一些新的抽象. (2认同)

Fat*_*tie 9

你这样做..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }
Run Code Online (Sandbox Code Playgroud)

99次100,这是唯一的方法.

永远不要试图在计算机代码中做一些"棘手"的事情.


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

继续语句是算法编程的关键.(就像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");
    }
Run Code Online (Sandbox Code Playgroud)

(首先请注意:像这样的裸块是编写漂亮代码的关键和重要部分,特别是如果你正在处理"算法"编程.)

再一次,这正是你脑子里的那个,对吗?这是写它的美妙方式,所以你有很好的直觉.

然而,不幸的是,在当前版本的objective-c(旁白 - 我不知道Swift,对不起),有一个可行的功能,它检查封闭块是否是一个循环.

在此输入图像描述

以下是你如何解决这个问题......

-(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");
    }
Run Code Online (Sandbox Code Playgroud)

所以不要忘记..

do {} while(false);

只是意味着"做一次阻止".

也就是说,写作do{}while(false);和简单写作完全没有区别{}.

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

在此输入图像描述

所以,你可能会在脑海中看到算法.你应该总是试着写下你脑子里的东西.(特别是如果你不清醒,因为那是漂亮的时候!:))

在"算法"项目中,这种情况发生了很多,在objective-c中,我们总是有一个宏...

#define RUNONCE while(false)
Run Code Online (Sandbox Code Playgroud)

......那么你可以这样做......

-(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");
    }
Run Code Online (Sandbox Code Playgroud)

有两点:

a,尽管目标-c检查一个继续语句所在的块的类型是愚蠢的,但是"打那个"很麻烦.所以这是一个艰难的决定.

b,在这个例子中,你应该缩进那个块吗?我在这样的问题上失眠了,所以我无法提出建议.

希望能帮助到你.


Rik*_*Rik 8

如果执行函数失败而不是返回false,则抛出异常.那么你的调用代码可能如下所示:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)
Run Code Online (Sandbox Code Playgroud)

当然我假设在你的原始例子中,执行步骤只会在步骤内发生错误的情况下返回false?

  • 使用异常来控制流通常被认为是不好的做法和臭代码 (3认同)

blg*_*lgt 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;
Run Code Online (Sandbox Code Playgroud)


cel*_*chk 6

在C++中(问题被标记为C和C++),如果你不能改变函数来使用异常,你仍然可以使用异常机制,如果你写一个小辅助函数,如

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}
Run Code Online (Sandbox Code Playgroud)

然后您的代码可以如下所示:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

如果你喜欢花哨的语法,你可以通过显式转换使它工作:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};
Run Code Online (Sandbox Code Playgroud)

然后你可以编写你的代码

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

  • -1在C++中使用异常进行正常流程是一种糟糕的编程习惯.在C++中,应该为特殊情况保留异常. (4认同)

gla*_*ert 6

对于C++ 11及更高版本,一个很好的方法可能是实现类似于D的范围(退出)机制的范围退出系统.

实现它的一种可能方法是使用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 })
Run Code Online (Sandbox Code Playgroud)

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

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

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;
Run Code Online (Sandbox Code Playgroud)

这些宏真的只是装饰.MakeScopeExit()可以直接使用.


Noc*_*wer 6

如果您的代码与示例一样简单,并且您的语言支持短路评估,那么您可以尝试这样做:

StepA() && StepB() && StepC() && StepD();
DoAlways();
Run Code Online (Sandbox Code Playgroud)

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


Gas*_*oin 6

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

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

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

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

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)


Bil*_*oor 5

几个答案暗示了我多次看到和使用的模式,尤其是在网络编程中。在网络堆栈中,通常存在一长串请求,其中任何一个请求都可能失败并停止进程。

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

我使用了一个宏来while(false)实现它,do { } once;常见的模式是:

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

这种模式相对容易阅读,并且允许使用能够正确破坏的对象,并且还避免了多次返回,从而使单步执行和调试变得更加容易。


Mac*_*cke 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();
Run Code Online (Sandbox Code Playgroud)

省略失败的变量会使代码过于模糊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();
Run Code Online (Sandbox Code Playgroud)

这是模糊的,但紧凑:

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

runThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)


Wha*_*ool 5

正如Rommik所提到的,你可以为此应用一个设计模式,但我会使用Decorator模式而不是策略,因为你想要链接调用.如果代码很简单,那么我会使用一个结构良好的答案来防止嵌套.但是,如果它很复杂或需要动态链接,那么Decorator模式是一个不错的选择.这是一个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;
    }
}
Run Code Online (Sandbox Code Playgroud)

关于设计模式的最佳书籍,恕我直言,是Head First Design Patterns.


Car*_*ght 5

这看起来像一个状态机,这很方便,因为您可以使用状态模式轻松实现它.

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

interface StepState{
public StepState performStep();
}
Run Code Online (Sandbox Code Playgroud)

实施工作如下:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}
Run Code Online (Sandbox Code Playgroud)

等等.然后你可以用以下方法替换大if条件:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();
Run Code Online (Sandbox Code Playgroud)