C++ 如何避免“如果”链?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24430504/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 00:44:25  来源:igfitidea点击:

How to avoid "if" chains?

c++cif-statementcontrol-flow

提问by ABCplus

Assuming I have this pseudo-code:

假设我有这个伪代码:

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

executeThisFunctionInAnyCase();

Functions executeStepXshould be executed if and only if the previous succeed. In any case, the executeThisFunctionInAnyCasefunction should be called at the end. I'm a newbie in programming, so sorry for the very basic question: is there a way (in C/C++ for example) to avoid that long ifchain producing that sort of "pyramid of code", at the expense of the code legibility?

executeStepX当且仅当前一个成功时才应执行函数。在任何情况下,executeThisFunctionInAnyCase都应该在最后调用该函数。我是编程的新手,对于这个非常基本的问题很抱歉:有没有办法(例如在 C/C++ 中)避免if产生那种“代码金字塔”的长链,以牺牲代码易读性为代价?

I know that if we could skip the executeThisFunctionInAnyCasefunction call, the code could be simplified as:

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

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

But the constraint is the executeThisFunctionInAnyCasefunction call. Could the breakstatement be used in some way?

但约束是executeThisFunctionInAnyCase函数调用。该break语句可以以某种方式使用吗?

回答by Shoe

You can use an &&(logic AND):

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

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

this will satisfy both of your requirements:

这将满足您的两个要求:

  • executeStep<X>()should evaluate only if the previous one succeeded (this is called short circuit evaluation)
  • executeThisFunctionInAnyCase()will be executed in any case
  • executeStep<X>()仅在前一个成功时才应评估(这称为短路评估
  • executeThisFunctionInAnyCase()在任何情况下都会被执行

回答by ltjax

Just use an additional function to get your second version to work:

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

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

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

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

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

Using either deeply nested ifs (your first variant) or the desire to break out of "part of a function" usually means you do need an extra function.

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

回答by cmaster - reinstate monica

Old school C programmers use gotoin this case. It is the one usage of gotothat's actually encouraged by the Linux styleguide, it's called the centralized function exit:

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

Some people work around using gotoby wrapping the body into a loop and breaking from it, but effectively both approaches do the same thing. The gotoapproach is better if you need some other cleanup only if executeStepA()was successfull:

有些人goto通过将主体包装成一个循环并从中中断来解决 using ,但实际上这两种方法都做同样的事情。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;
}

With the loop approach you would end up with two levels of loops in that case.

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

回答by John Wu

This is a common situation and there are many common ways to deal with it. Here's my attempt at a canonical answer. Please comment if I missed anything and I'll keep this post up to date.

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

This is an Arrow

这是一个箭头

What you are discussing is known as the arrow anti-pattern. It is called an arrow because the chain of nested ifs form code blocks that expand farther and farther to the right and then back to the left, forming a visual arrow that "points" to the right side of the code editor pane.

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

Flatten the Arrow with the Guard

用守卫压平箭头

Some common ways of avoiding the Arrow are discussed here. The most common method is to use a guardpattern, in which the code handles the exception flows first and then handles the basic flow, e.g. instead of

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

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... you'd use....

……你会用……

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

When there is a long series of guards this flattens the code considerably as all the guards appear all the way to the left and your ifs are not nested. In addition, you are visually pairing the logic condition with its associated error, which makes it far easier to tell what is going on:

当有很长的守卫系列时,这会使代码显着变平,因为所有守卫都一直出现在左侧并且您的 if 没有嵌套。此外,您在视觉上将逻辑条件与其相关错误配对,这使得更容易判断发生了什么:

Arrow:

箭:

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

Guard:

警卫:

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

This is objectively and quantifiably easier to read because

这在客观上和量化上更容易阅读,因为

  1. The { and } characters for a given logic block are closer together
  2. The amount of mental context needed to understand a particular line is smaller
  3. The entirety of logic associated with an if condition is more likely to be on one page
  4. The need for the coder to scroll the page/eye track is greatly lessened
  1. 给定逻辑块的 { 和 } 字符靠得更近
  2. 理解特定行所需的心理上下文量较小
  3. 与 if 条件相关的整个逻辑更有可能出现在一页上
  4. 编码员滚动页面/眼动轨迹的需要大大减少

How to add common code at the end

最后如何添加公共代码

The problem with the guard pattern is that it relies on what is called "opportunistic return" or "opportunistic exit." In other words, it breaks the pattern that each and every function should have exactly one point of exit. This is a problem for two reasons:

守卫模式的问题在于它依赖于所谓的“机会回归”或“机会退出”。换句话说,它打破了每个函数都应该只有一个退出点的模式。这是一个问题,原因有两个:

  1. It rubs some people the wrong way, e.g. people who learned to code on Pascal have learned that one function = one exit point.
  2. It does not provide a section of code that executes upon exit no matter what, which is the subject at hand.
  1. 它让一些人误会,例如,学习在 Pascal 上编码的人已经知道一个函数 = 一个退出点。
  2. 无论如何它不提供在退出时执行的代码段,这是手头的主题。

Below I've provided some options for working around this limitation either by using language features or by avoiding the problem altogether.

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

Option 1. You can't do this: use finally

选项 1. 你不能这样做:使用 finally

Unfortunately, as a c++ developer, you can't do this. But this is the number one answer for languages that contain a finally keyword, since this is exactly what it is for.

不幸的是,作为 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();
}

Option 2. Avoid the issue: Restructure your functions

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

You can avoid the problem by breaking the code into two functions. This solution has the benefit of working for any language, and additionally it can reduce cyclomatic complexity, which is a proven way to reduce your defect rate, and improves the specificity of any automated unit tests.

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

Here's an example:

下面是一个例子:

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

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Option 3. Language trick: Use a fake loop

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

Another common trick I see is using while(true) and break, as shown in the other answers.

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

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

While this is less "honest" than using goto, it is less prone to being messed up when refactoring, as it clearly marks the boundaries of logic scope. A naive coder who cuts and pastes your labels or your gotostatements can cause major problems! (And frankly the pattern is so common now I think it clearly communicates the intent, and is therefore not "dishonest" at all).

虽然这比 using 更不“诚实” goto,但在重构时不太容易搞砸,因为它清楚地标记了逻辑范围的边界。剪切和粘贴您的标签或goto语句的天真编码员可能会导致重大问题!(坦率地说,这种模式现在非常普遍,我认为它清楚地传达了意图,因此根本不是“不诚实”)。

There are other variants of this options. For example, one could use switchinstead of while. Any language construct with a breakkeyword would probably work.

此选项还有其他变体。例如,可以使用switch代替while。任何带有break关键字的语言结构都可能起作用。

Option 4. Leverage the object life cycle

选项 4. 利用对象生命周期

One other approach leverages the object life cycle. Use a context object to carry around your parameters (something which our naive example suspiciously lacks) and dispose of it when you're done.

另一种方法利用对象生命周期。使用上下文对象来携带您的参数(我们的幼稚示例可疑缺乏的东西)并在您完成后处理它。

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
}

Note: Be sure you understand the object life cycle of your language of choice. You need some sort of deterministic garbage collection for this to work, i.e. you have to know when the destructor will be called. In some languages you will need to use Disposeinstead of a destructor.

注意:确保您了解所选语言的对象生命周期。您需要某种确定性的垃圾收集才能使其工作,即您必须知道何时调用析构函数。在某些语言中,您将需要使用Dispose而不是析构函数。

Option 4.1. Leverage the object life cycle (wrapper pattern)

选项 4.1。利用对象生命周期(包装器模式)

If you're going to use an object-oriented approach, may as well do it right. This option uses a class to "wrap" the resources that require cleanup, as well as its other operations.

如果您打算使用面向对象的方法,不妨做对。此选项使用一个类来“包装”需要清理的资源及其其他操作。

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

Again, be sure you understand your object life cycle.

同样,请确保您了解您的对象生命周期。

Option 5. Language trick: Use short-circuit evaluation

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

Another technique is to take advantage of short-circuit evaluation.

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

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

This solution takes advantage of the way the && operator works. When the left hand side of && evaluates to false, the right hand side is never evaluated.

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

This trick is most useful when compact code is required and when the code is not likely to see much maintenance, e.g you are implementing a well-known algorithm. For more general coding the structure of this code is too brittle; even a minor change to the logic could trigger a total rewrite.

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

回答by Cheers and hth. - Alf

Just do

做就是了

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

It's that simple.

就这么简单。



Due to three edits that each has fundamentallychanged the question (four if one counts the revision back to version #1), I include the code example I'm answering to:

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

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

executeThisFunctionInAnyCase();

回答by Matthieu M.

There is actually a way to defer actions in C++: making use of an object's destructor.

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

Assuming that you have access to C++11:

假设您可以访问 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

And then using that utility:

然后使用该实用程序:

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

    // ...

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

    // ...

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

    // ...

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

    // ...

    return 4;
} // foo

回答by Debasis

There's a nice technique which doesn't need an additional wrapper function with the return statements (the method prescribed by Itjax). It makes use of a do while(0)pseudo-loop. The while (0)ensures that it is actually not a loop but executed only once. However, the loop syntax allows the use of the break statement.

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

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

回答by Debasis

You could also do this:

你也可以这样做:

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

This way you have a minimal linear growth size, +1 line per call, and it's easily maintenable.

通过这种方式,您的线性增长规模最小,每次调用 +1 行,并且易于维护。



EDIT: (Thanks @Unda) Not a big fan because you loose visibility IMO :

编辑:(感谢@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();

回答by sampathsris

Would this work? I think this is equivalent with your code.

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

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

回答by ClickRick

Assuming the desired code is as I currently see it:

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

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

I would say that the correct approach, in that it's the simplest to read and easiest to maintain, would have fewer levels of indentation, which is (currently) the stated purpose of the question.

我会说正确的方法,因为它是最简单的阅读和最容易维护的方法,缩进级别更少,这是(目前)问题的陈述目的。

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

This avoids anyneed for gotos, exceptions, dummy whileloops, or other difficult constructs and simply gets on with the simple job at hand.

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