ChatGPT解决这个技术问题 Extra ChatGPT

如何避免“if”链?

假设我有这个伪代码:

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 语句吗?

@FrédéricHamidi 错错错了!永远不要说用异常来驱动你的程序流是好的!由于太多原因,例外绝对不适合此目的。
@Piotr,我被 Python 宠坏了(这实际上鼓励了这一点)。我知道异常不应该用于 C++ 中的流控制,但这里真的是流控制吗?不能将返回 false 的函数视为类似于异常情况吗?
这取决于程序的语义。 false 返回可能很正常。
我已将您的问题回滚到第一次修订。在收到一定数量的问题(> 0)后,您不应该彻底改变您的问题,因为这会使当时给出的所有答案无效,并会造成混乱。而是打开一个新问题。
我希望所有“新手程序员”都会问这样的设计问题。

D
David

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

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

这将满足您的两个要求:

executeStep() 仅在前一个成功时才应评估(这称为短路评估)

executeThisFunctionInAnyCase() 在任何情况下都会被执行


这在语义上是正确的(实际上,我们希望所有条件都为真)以及非常好的编程技术(短路评估)。此外,这可以用于创建函数会弄乱代码的任何复杂情况。
@RobAu:那么对于初级程序员来说,最终看到依赖于快捷评估的代码将是一件好事,这可能会促使他们研究这个主题,这反过来又会帮助他们最终成为高级程序员。显然是双赢:体面的代码和有人从阅读中学到了一些东西。
这应该是最佳答案
@RobAu:这不是利用一些晦涩的语法技巧的黑客攻击,它在几乎所有编程语言中都非常惯用,以至于成为无可争议的标准做法。
此解决方案仅适用于条件实际上是简单的函数调用的情况。在实际代码中,这些条件可能有 2-5 行长(并且它们本身是许多其他 &&|| 的组合),因此在不影响可读性的情况下,您不可能将它们加入单个 if 语句。将这些条件转移到外部函数并不总是那么容易,因为它们可能依赖于许多先前计算的局部变量,如果您尝试将每个变量作为单独的参数传递,这将造成可怕的混乱。
l
ltjax

只需使用附加功能即可使您的第二个版本正常工作:

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(您的第一个变体)或打破“函数的一部分”的愿望通常意味着您确实需要一个额外的函数。


+1终于有人发布了一个理智的答案。在我看来,这是最正确、最安全、最易读的方式。
+1 这是“单一职责原则”的一个很好的例证。函数 foo 在一系列相关条件和操作中发挥作用。函数 bar 与决策完全分离。如果我们查看条件和操作的详细信息,可能会发现 foo 仍然做得太多,但目前这是一个很好的解决方案。
缺点是 C 没有嵌套函数,因此如果这 3 个步骤需要使用来自 bar 的变量,您将不得不手动将它们作为参数传递给 foo。如果是这种情况,并且如果 foo 只被调用一次,我会错误地使用 goto 版本以避免定义两个紧密耦合的函数,这些函数最终不会非常可重用。
不确定 C 的短路语法,但在 C# 中 foo() 可以写成 if (!executeStepA() || !executeStepB() || !executeStepC()) return
@user1598390 Goto 一直在使用,尤其是在系统编程中,当您需要展开大量设置代码时。
c
cmaster - reinstate monica

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

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 的问题,但实际上这两种方法都做同样的事情。如果只有在 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;
}

在这种情况下,使用循环方法最终会得到两个级别的循环。


+1。很多人看到 goto 并立即认为“这是糟糕的代码”,但它确实有其有效用途。
除了从维护的角度来看,这实际上是非常混乱的代码。特别是当有多个标签时,代码也变得更难阅读。有更优雅的方法来实现这一点:使用函数。
-1 我看到了 goto,我什至不用想就知道这是个糟糕的代码。我曾经不得不保持这样,这非常讨厌。 OP 在问题的末尾提出了一个合理的 C 语言替代方案,我将其包含在我的答案中。
goto 的这种有限的、独立的使用没有任何问题。但要注意,goto 是一种入门药物,如果你不小心,你有一天会意识到没有其他人会用脚吃意大利面,而在你认为“我可以解决这个问题”之后,你已经这样做了 15 年否则带有标签的噩梦般的错误......”
我用这种风格写了大概十万行非常清晰、易于维护的代码。要了解的两个关键事项是(1)纪律!为每个函数的布局建立一个明确的指导方针,并且只在极端需要时才违反它,并且 (2) 了解我们在这里所做的是用没有异常的语言模拟异常throw 在很多方面都比 goto 更糟糕,因为使用 throw 甚至不清楚从本地上下文您将在哪里结束!对 goto 样式的控制流使用与异常相同的设计注意事项。
J
John Wu

这是一种常见的情况,有很多常用的方法来处理它。这是我对规范答案的尝试。如果我遗漏了什么,请发表评论,我会及时更新这篇文章。

这是一个箭头

您所讨论的内容称为 arrow anti-pattern。之所以称为箭头,是因为嵌套的 ifs 链形成了代码块,这些代码块向右扩展,然后再向左扩展,形成一个“指向”代码编辑器窗格右侧的可视箭头。

用卫兵压平箭

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

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

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

您可以通过将代码分成两个函数来避免该问题。此解决方案的优点是适用于任何语言,此外它还可以减少 cyclomatic complexity,这是一种降低缺陷率的行之有效的方法,并提高了任何自动化单元测试的特异性。

这是一个例子:

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. 语言技巧:使用短路评估

另一种技术是利用 short-circuit evaluation

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

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

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


最后? C++ 没有 finally 子句。带有析构函数的对象用于您认为需要 finally 子句的情况。
编辑以适应上面的两条评论。
对于琐碎的代码(例如在我的示例中),嵌套模式可能更容易理解。对于真实世界的代码(可能跨越几页),保护模式客观上更容易阅读,因为它需要更少的滚动和更少的眼动跟踪,例如从 { 到 } 的平均距离更短。
我已经看到了代码在 1920 x 1080 屏幕上不再可见的嵌套模式...尝试找出如果第三个操作失败将执行的错误处理代码...我使用了 do { ... } 而(0) 相反,因此您不需要最后的休息(另一方面,而 (true) { ... } 允许“继续”重新开始。
您的选项 4 实际上是 C++ 中的内存泄漏(忽略轻微的语法错误)。在这种情况下不要使用“new”,只需说“MyContext myContext;”。
C
Cheers and hth. - Alf

做就是了

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

就是这么简单。

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

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

executeThisFunctionInAnyCase();

我在回答问题的第一个版本时回答了这个问题(更简洁),在经过黑暗种族在轨道上的一些评论和编辑之后,它在被蜥蜴比尔删除之前得到了 20 个赞成票。
@Cheersandhth-Alf:我不敢相信它被 mod 删除了。这太糟糕了。 (+1)
我为你的勇气+1! (3次确实很重要:D)。
新手程序员了解诸如具有多个布尔“和”的订单执行之类的东西是非常重要的,这在不同的语言中有何不同等等。这个答案是一个很好的指针。但这只是普通商业编程中的“非首发”。它不简单,不可维护,不可修改。当然,如果您只是为自己编写一些快速的“牛仔代码”,请执行此操作。但它只是有点“与今天实践的日常软件工程没有联系”。顺便说一句,你不得不在这里忍受荒谬的“编辑混乱”,@cheers :)
@JoeBlow:我同意 Alf - 我发现 && 列表更清晰。我通常将条件分解为单独的行,并在每行后面添加一个 // explanation.... 最后,它的代码要少得多,一旦您了解 && 的工作原理,就不需要持续的脑力劳动。我的印象是,大多数专业的 C++ 程序员都会熟悉这一点,但正如你所说,在不同的行业/项目中,重点和经验是不同的。
M
Matthieu M.

实际上有一种方法可以在 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

这对我来说只是完全的混淆。我真的不明白为什么这么多 C++ 程序员喜欢通过尽可能多的语言特性来解决问题,直到每个人都忘记了你实际解决的是什么问题:他们不再关心,因为他们是现在对使用所有这些异国语言功能非常感兴趣。从那以后,你可以让自己忙上几天和几周,编写元代码,然后维护元代码,然后编写元代码来处理元代码。
@Lundin:好吧,我不明白人们如何对一旦引入早期继续/中断/返回或抛出异常就会中断的脆弱代码感到满意。另一方面,该解决方案在面对未来的维护时具有弹性,并且仅依赖于在展开期间执行析构函数这一事实,这是 C++ 最重要的特性之一。至少可以说,虽然它是为所有标准容器提供动力的基本原则,但将其限定为异国情调是很有趣的。
@Lundin:Matthieu 代码的好处是即使 foo(); 抛出异常,executeThisFunctionInAnyCase(); 也会执行。在编写异常安全代码时,最好将所有此类清理函数放在析构函数中。
@Brian 然后不要在 foo() 中抛出任何异常。如果你这样做了,抓住它。问题解决了。通过修复它们来修复错误,而不是通过编写解决方法。
@Lundin:Defer 类是一个可重用 小段代码,可让您以异常安全的方式进行任何块结束清理。它通常被称为 范围保护。是的,范围保护的任何使用都可以用其他更手动的方式表示,就像任何 for 循环都可以表示为一个块和一个 while 循环,而后者又可以用 ifgoto 表示,如果你愿意,可以用汇编语言表达,或者对于那些真正的大师,通过特殊短咕噜声和圣歌的蝴蝶效应引导的宇宙射线改变记忆中的位。但是,为什么要这样做。
T
Tim Visée

有一种很好的技术,它不需要带有返回语句的附加包装函数(Itjax 规定的方法)。它使用 do while(0) 伪循环。 while (0) 确保它实际上不是一个循环,而是只执行一次。但是,循环语法允许使用 break 语句。

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

在我看来,使用具有多个返回的函数是相似的,但更具可读性。
是的,它确实更具可读性......但是从效率的角度来看,使用 do {} while (0) 构造可以避免额外的函数调用(参数传递和返回)开销。
您仍然可以自由地创建函数 inline。无论如何,这是一个很好的技术,因为它不仅能解决这个问题。
@Lundin您必须考虑代码局部性,将代码分散到太多地方也有问题。
以我的经验,这是一个非常不寻常的成语。我需要一段时间才能弄清楚到底发生了什么,当我审查代码时,这是一个不好的迹象。鉴于它似乎与其他更常见且因此更具可读性的方法相比没有优势,我无法签署它。
佚名

你也可以这样做:

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

这是关于 push_back() 中的意外函数调用,但你还是修复了它:)
我很想知道为什么这会被否决。假设执行步骤确实是对称的,就像它们看起来的那样,那么它是干净且可维护的。
虽然这可能看起来更干净。对人来说可能更难理解,对编译器来说肯定更难理解!
将您的函数更多地视为数据通常是一个好主意——一旦您这样做了,您还会注意到后续的重构。更好的是,您正在处理的每个部分都是对象引用,而不仅仅是函数引用——这将使您有更多的能力来改进您的代码。
稍微过度设计了一个微不足道的案例,但这种技术肯定有一个其他人没有的好特性:您可以更改执行顺序和在运行时调用的函数[数量],这很好:)
s
sampathsris

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

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

当使用像这样的相同变量时,我通常调用变量 ok
我很想知道为什么会投反对票。这里出了什么问题?
将您的答案与圈复杂度的短路方法进行比较。
C
ClickRick

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

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

这避免了任何goto、异常、虚拟 while 循环或其他困难构造的需求,并且只需继续手头的简单工作即可。


使用循环时,通常可以使用 returnbreak 跳出循环,而无需引入额外的“标志”变量。在这种情况下,使用 goto 将同样无害 - 请记住,您正在用额外的 goto 复杂性换取额外的可变变量复杂性。
@hugomg 变量在原始问题中。这里没有额外的复杂性。对这个问题做了一些假设(例如,在编辑代码中需要变量),所以它们被保留了。如果不需要它们,则可以简化代码,但鉴于问题的不完整性质,没有其他可以有效地做出的假设。
非常有用的方法,尤其是。供自称 newbie 的人使用,它提供了一种更清洁的解决方案,没有任何缺点。我注意到它也不依赖于 steps 具有相同的签名甚至是函数而不是块。即使在更复杂的方法有效的情况下,我也可以看到这被用作第一遍重构。
o
ouah

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

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


不是我反对它,但这将是对循环结构的滥用,因为它的效果是当前想要的,但不可避免地会导致痛苦。可能是下一个开发人员在您转移到另一个项目后必须在 2 年内维护它。
@ClickRick 使用 do .. while (0) 进行宏定义也是滥用循环,但它被认为是可以的。
也许,但有更清洁的方法来实现它。
@ClickRick 我回答的唯一目的是回答是否可以以某种方式使用 break 语句,答案是肯定的,我回答中的第一句话表明这可能不是使用的解决方案。
这个答案应该只是一个评论
N
Niall

您可以将所有 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();

A
Alexander Oh

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

if you dislike goto && you dislike do { } while (0) && you like C++ ...抱歉,无法抗拒,但最后一个条件失败,因为问题被标记为 c 以及 c++
@ClickRick 总是很难取悦所有人。在我看来,没有像 C/C++ 这样的东西,你通常在其中任何一个中编写代码,而另一个的用法是不受欢迎的。
R
Roman Mik

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

通常,当你看到很多 IF's & else 在您的代码中,这是实现策略设计模式 (http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net) 或其他模式组合的机会。

我敢肯定有其他方法可以编写一个长长的 if/else 列表,但我怀疑它们会改变任何东西,除了链对你来说看起来很漂亮(但是,情人眼中的美仍然适用于代码也:-) ) 。您应该关心的事情是(在 6 个月内,当我有一个新情况并且我不记得有关此代码的任何内容时,我是否能够轻松添加它?或者如果链发生变化,如何快速和无错误我会实施吗)


C
Community

你就这样做..

coverConditions();
executeThisFunctionInAnyCase();

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

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

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

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

continue 语句在算法编程中至关重要。 (很像,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,抱歉)有一个可笑的功能,它检查封闭块是否是一个循环。

https://i.stack.imgur.com/glq81.png

这就是你如何解决这个问题......

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

所以不要忘记..

做 { } 而(假);

只是意味着“做这个块一次”。

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

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

https://i.stack.imgur.com/trGXZ.png

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

在经常发生这种情况的“算法”项目中,在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,在这个例子中,你应该缩进那个块吗?我会因为这样的问题而失眠,所以我无法提供建议。

希望能帮助到你。


您错过了 second best voted answer
你放赏金是为了获得更多的代表吗? :)
除了将所有这些注释都放在 if 中,您还可以使用更具描述性的函数名称并将注释放在函数中。
呸。我将在任何一天都使用 1 行解决方案,该解决方案通过短路评估(已经在语言中使用了 20 多年并且众所周知)简洁明了。我想我们都可以同意我们很高兴不互相合作。
R
Rik

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

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

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


使用异常来控制流通常被认为是不好的做法和臭代码
b
blgt

已经有很多很好的答案,但它们中的大多数似乎都牺牲了一些(诚然很少)灵活性。不需要这种权衡的一种常见方法是添加状态/持续变量。当然,价格是要跟踪的一个额外价值:

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;
@user3253359 在很多情况下,是的,您可以这样做。这是一个概念演示;在工作代码中,我们会尽量简化它
+1 问题中规定的在 c 中有效的少数干净且可维护的答案之一。
c
celtschk

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

将值检查重构为异常不一定是一个好方法,展开异常会有相当大的开销。
-1 像这样在 C++ 中对正常流程使用异常是糟糕的编程习惯。在 C++ 中,应为异常情况保留异常。
从问题文本(我强调):“当且仅当前一个成功时,才应该执行函数 executeStepX。”换句话说,返回值用于指示失败。也就是说,这是错误处理(人们希望失败是例外的)。错误处理正是发明异常的目的。
没有。首先,创建异常是为了允许错误传播,而不是错误处理;其次,“当且仅当前一个成功时,才应该执行函数 executeStepX。”并不意味着前一个函数返回的布尔值 false 表示明显异常/错误的情况。因此,您的陈述是不合理的。错误处理和流程清理可以通过许多不同的方式实现,异常是允许错误传播和异地错误处理的工具,并且擅长于此。
N
Noctis Skytower

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

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

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


事实上,我编辑了我的问题以更好地解释该主题,但它被拒绝不使大多数答案无效。 :\
我是 SO 的新用户,也是新手程序员。那么 2 个问题:是否存在像您所说的那样的另一个问题会因为这个问题而被标记为重复的风险?另一点是:新手 SO 用户/程序员如何在所有这些之间选择最佳答案(我想几乎是好的..)?
g
glampert

对于 C++11 及更高版本,一个不错的方法可能是实现类似于 D's 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() 可以直接使用。


不需要宏来完成这项工作。对于作用域 lambda,[=] 通常是错误的。
是的,宏只是为了装饰,可以扔掉。但是您不会说按价值捕获是最安全的“通用”方法吗?
否:如果您的 lambda 不会超出创建 lambda 的当前范围,请使用 [&]:它是安全的,并且不会令人惊讶。仅当 lambda(或副本)的生存时间超过声明点的范围时才按值捕获...
是的,这是有道理的。我会改变它。谢谢!
r
rageandqq

为什么没有人给出最简单的解决方案? :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();

M
Macke

假设您不需要单独的条件变量,反转测试并使用 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();

C
CarrKnight

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

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

interface StepState{
public StepState performStep();
}

一个实现将按如下方式工作:

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

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

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

W
What Would Be Cool

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

https://i.stack.imgur.com/lkall.png

这是一个示例 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 的某些部分的深度和长度时。它都可能变得非常脆弱,因此使用起来风险很高。不要误会我的意思,一些优化方案可能会导致您将其撕掉并返回到 ifs,但 99% 的情况下这很好。但关键是当你达到那个水平时,你不需要关心可维护性,你需要性能。
B
Bill Door

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

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

我为 while(false) 使用了一个宏来使其成为 do { } once; 常见的模式是:

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

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


J
Joe

为了改进 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我正在寻找这个答案,所以我不必发布它。您应该在 deferred 的构造函数中完美转发 functor,无需强制使用 move
@Yakk 将构造函数更改为转发构造函数
A
AxFab

似乎您想从一个块中完成所有呼叫。正如其他人所建议的那样,您应该使用 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();

幸运的是,调试器会为您发现它们。如果您正在调试而不是单步执行代码,那么您做错了。
我不明白你的意思,为什么我不能使用单步?
A
Arkady

因为您在执行之间也有[...代码块...],我猜您有内存分配或对象初始化。这样,您必须关心在退出时清理所有已初始化的内容,如果遇到问题并且任何函数都将返回 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 都初始化一些东西,分配内存等等。最好确保一切都会被清理。


H
Hot Licks

这是我在 C-whatever 和 Java 中多次使用的技巧:

do {
    if (!condition1) break;
    doSomething();
    if (!condition2) break;
    doSomethingElse()
    if (!condition3) break;
    doSomethingAgain();
    if (!condition4) break;
    doYetAnotherThing();
} while(FALSE);  // Or until(TRUE) or whatever your language likes

为了清晰起见,我更喜欢它而不是嵌套 ifs,尤其是在正确格式化并为每个条件提供清晰注释的情况下。


在 Java 中,我会通过返回和 finally 块来解决这个问题。
K
Karsten

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

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

如果你编写这样的代码,你就会以某种方式走错方向。我不会认为拥有这样的代码是“问题”,而是拥有如此凌乱的“架构”。

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


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