终止带有 else 子句的 if … else if 有什么好处?

What is the benefit of terminating if … else if constructs with an else clause?

我们的组织有一个必需的编码规则(没有任何解释):

if … else if constructs should be terminated with an else clause

示例 1:

if ( x < 0 )
{
   x = 0;
} /* else not needed */

示例 2:

if ( x < 0 )
{
    x = 0;
}
else if ( y < 0 )
{
    x = 3;
}
else    /* this else clause is required, even if the */
{       /* programmer expects this will never be reached */
        /* no change in value of x */
}

这是为了处理什么极端情况而设计的?

我还担心的原因是 示例 1 不需要 else示例 2 需要。如果原因是可重用性和可扩展性,我认为在这两种情况下都应该使用 else

贵公司遵循了 MISRA 编码指南。这些指南有几个版本包含此规则,但来自 MISRA-C:2004:

Rule 14.10 (required): All if … else if constructs shall be terminated with an else clause.

This rule applies whenever an if statement is followed by one or more else if statements; the final else if shall be followed by an else statement. In the case of a simple if statement then the else statement need not be included. The requirement for a final else statement is defensive programming. The else statement shall either take appropriate action or contain a suitable comment as to why no action is taken. This is consistent with the requirement to have a final default clause in a switch statement. For example this code is a simple if statement:

if ( x < 0 )
{
 log_error(3);
 x = 0;
} /* else not needed */

whereas the following code demonstrates an if, else if construct

if ( x < 0 )
{
 log_error(3);
 x = 0;
}
else if ( y < 0 )
{
 x = 3;
}
else /* this else clause is required, even if the */
{ /* programmer expects this will never be reached */
 /* no change in value of x */
}

MISRA-C:2012, which supersedes the 2004 version and is the current recommendation for new projects, .

示例 1: 在单个 if 语句中,程序员可能需要检查 n 个条件并执行单个操作。

if(condition_1 || condition_2 || ... condition_n)
{
   //operation_1
}

在正常使用中,当使用 if 时,并不总是需要执行操作。

示例 2: 这里程序员检查 n 个条件并执行多个操作。在常规用法中,if..else if 类似于 switch,您可能需要执行类似默认的操作。因此,根据 misra 标准

,需要使用 else
if(condition_1 || condition_2 || ... condition_n)
{
   //operation_1
}
else if(condition_1 || condition_2 || ... condition_n)
{
  //operation_2
}
....
else
{
   //default cause
}

这些出版物的当前和过去版本可通过 MISRA webstore (via).

购买

这样做是为了使代码更具可读性,供以后参考,并向后来的审阅者表明,最后 else 处理的其余案例是 什么都不做 个案例,这样它们就不会在第一眼以某种方式被忽视。

这是一个很好的编程习惯,它使代码 可重用 extend-able.

This is the equivalent of requiring a default case in every switch.

这个额外的 else 将减少您程序的 code coverage


根据我将 linux 内核或 android 代码移植到不同平台的经验,很多时候我们都会做错事,在 logcat 中我们会看到一些错误,例如

if ( x < 0 )
{
    x = 0;
}
else if ( y < 0 )
{
    x = 3;
}
else    /* this else clause is required, even if the */
{       /* programmer expects this will never be reached */
        /* no change in value of x */
        printk(" \n [function or module name]: this should never happen \n");

        /* It is always good to mention function/module name with the 
           logs. If you end up with "this should never happen" message
           and the same message is used in many places in the software
           it will be hard to track/debug.
        */
}

正如另一个答案中提到的,这是来自 MISRA-C 编码指南。目的是防御性编程,这是 mission-critical 编程中经常使用的概念。

也就是说,每个 if - else if 必须以 else 结尾,每个 switch 必须以 default 结尾。

这有两个原因:

  • Self-documenting代码。如果您写 else 但将其留空,则表示:"I have definitely considered the scenario when neither if nor else if are true"。

    不写 else 表示:"either I considered the scenario where neither if nor else if are true, or I completely forgot to consider it and there's potentially a fat bug right here in my code".

  • 停止失控代码。在 mission-critical 软件中,您需要编写健壮的程序,以应对极不可能发生的情况。所以你可以看到像

    这样的代码
    if (mybool == TRUE) 
    {
    } 
    else if (mybool == FALSE) 
    {
    }
    else
    {
      // handle error
    }
    

    这段代码对于 PC 程序员和计算机科学家来说是完全陌生的,但它在 mission-critical 软件中非常有意义,因为它捕获了 "mybool" 因任何原因而损坏的情况.

    从历史上看,由于 EMI/noise,您会担心 RAM 内存损坏。今天这不是什么大问题。更有可能的是,代码中其他地方的错误导致内存损坏:指向错误位置的指针、array-out-of-bounds 错误、堆栈溢出、代码失控等

    所以大多数时候,当你在实现阶段写了错误时,像这样的代码回来打你自己的脸。这意味着它也可以用作调试技术:您正在编写的程序会在您编写错误时告诉您。


编辑

关于为什么在每个if之后都不需要else

if-elseif-else if-else 完全涵盖了变量可能具有的所有可能值。但是一个简单的 if 语句不一定涵盖所有可能的值,它有更广泛的用途。大多数情况下,您只想检查某个条件,如果不满足,则什么也不做。那么为了覆盖else案例而写防御性编程根本就没有意义。

此外,如果您在每个 if.

之后写一个空的 else,代码就会完全混乱

MISRA-C:2012 15.7 没有说明为什么不需要 else,它只是说明:

Note: a final else statement is not required for a simple if statement.

从逻辑上讲,任何测试都意味着两个分支。如果是真的怎么办,如果是假的怎么办。

对于任何一个分支都没有功能的情况,添加一个关于为什么它不需要有功能的评论是合理的。

这可能对下一位维护程序员有帮助。他们不必搜索太多就可以确定代码是否正确。你可以 Prehunt the Elephant.

就我个人而言,它对我有帮助,因为它迫使我查看 else 案例并对其进行评估。这可能是不可能的情况,在这种情况下,我可能会因为违反合同而抛出异常。它可能是良性的,在这种情况下,评论可能就足够了。

您的里程可能会有所不同。

基本原因可能是代码覆盖率和隐含的 else:如果条件不为真,代码将如何运行?对于真正的测试,您需要一些方法来查看您是否在条件为 false 的情况下进行了测试。如果您拥有的每个测试用例都经过 if 子句,那么您的代码可能会因为您未测试的条件而在现实世界中出现问题。

但是,某些条件可能与示例 1 类似,例如税收 return:"If the result is less than 0, enter 0." 您仍然需要在条件为假的情况下进行测试。

只是一个简短的解释,因为我大约 5 年前就这样做了。

(对于大多数语言)没有句法要求包含 "null" else 语句(和不必要的 {..}),并且在 "simple little programs" 中没有必要。但真正的程序员不会编写 "simple little programs",而且,同样重要的是,他们不会编写将使用一次然后丢弃的程序。

当一个人写 if/else:

if(something)
  doSomething;
else
  doSomethingElse;

一切看起来都很简单,甚至连添加 {..}.

的意义都看不到

但是有一天,几个月后,其他程序员(你永远不会犯这样的错误!)将需要 "enhance" 程序并添加一条语句。

if(something)
  doSomething;
else
  doSomethingIForgot;
  doSomethingElse;

突然 doSomethingElse 有点忘了它应该在 else 腿上。

所以你是一个优秀的小程序员,你总是使用 {..}。但是你写:

if(something) {
  if(anotherThing) {
    doSomething;
  }
}

在那个新孩子进行午夜修改之前一切都很好:

if(something) {
  if(!notMyThing) {
  if(anotherThing) {
    doSomething;
  }
  else {
    dontDoAnything;  // Because it's not my thing.
  }}
}

是的,它的格式不正确,但项目中一半的代码也是如此,"auto formatter" 被所有 #ifdef 语句搞砸了。当然,实际代码远比这个玩具示例复杂。

不幸的是(或不是),我已经离开这种事情好几年了,所以我没有一个新鲜的 "real" 例子——上面是(显然) 做作而且有点矫揉造作。

我目前正在与 PHP 合作。创建注册表单和登录表单。我只是纯粹使用 if 和 else。没有其他如果或任何不必要的东西。

如果用户单击提交按钮 -> 它会转到下一个 if 语句...如果用户名少于 'X' 个字符数,则发出警报。如果成功则检查密码长度等。

不需要额外的代码,例如 else,如果它可以消除服务器加载时间检查所有额外代码的可靠性。

我想补充——并部分反驳——之前的答案。虽然以类似开关的方式使用 if-else if 肯定是很常见的,它应该涵盖表达式的所有可想到的值范围,但绝不保证完全涵盖任何范围的可能条件。对于 switch 结构本身也可以这样说,因此需要使用默认子句,它捕获所有剩余的值,并且如果不需要的话,可以用作断言保护。

这个问题本身就有一个很好的反例:第二个条件与 x 完全无关(这就是为什么我更喜欢更灵活的基于 if 的变体而不是基于开关的变体的原因)。从示例中可以明显看出,如果满足条件 A,则应将 x 设置为某个值。如果不满足 A,则测试条件 B。如果满足,则 x 应该接收另一个值。如果A和B都不满足,那么x应该保持不变。

这里我们可以看到应该使用一个空的else分支来注释程序员对reader的意图。

另一方面,我不明白为什么必须有一个 else 子句,特别是对于最新的和最里面的 if 语句。 在 C 中,没有 'else if' 这样的东西。只有 if 和 else. 相反,该结构应该以这种方式正式缩进(我应该将左花括号放在它们自己的行上,但我不喜欢那样):

if (A) {
    // do something
}
else {
    if (B) {
        // do something else (no pun intended)
    }
    else {
        // don't do anything here
    }
}

如果任何标准碰巧要求每个分支都用花括号括起来,那么如果它同时提到“if ... else if constructs”就会自相矛盾。

任何人都可以想象深层嵌套的 if else 树的丑陋,see here on a side note。现在想象一下,这个结构可以任意扩展到任何地方。然后在最后要求一个 else 子句,而不是在其他任何地方,变得荒谬。

if (A) {
    if (B) {
        // do something
    }
    // you could to something here
}
else {
    // or here
    if (B) { // or C?
        // do something else (no pun intended)
    }
    else {
        // don't do anything here, if you don't want to
    }
    // what if I wanted to do something here? I need brackets for that.
}

最后,他们要准确定义“if ... else if construct”的含义

我们的软件不是关键任务,但由于防御性编程,我们也决定使用此规则。 我们在理论上不可达的代码中加入了抛出异常(switch + if-else)。由于软件快速失败,它为我们节省了很多时间,例如当添加了一个新类型而我们忘记更改一两个 if-else 或 switch 时。作为奖励,它非常容易找到问题。

好吧,我的例子涉及未定义的行为,但有时有些人想花哨而失败,看看:

int a = 0;
bool b = true;
uint8_t* bPtr = (uint8_t*)&b;
*bPtr = 0xCC;
if(b == true)
{
    a += 3;
}
else if(b == false)
{
    a += 5;
}
else
{
    exit(3);
}

您可能永远不会期望 bool 既不是 true 也不是 false,但它可能会发生。我个人认为这是由决定做一些花哨的事情的人造成的问题,但额外的 else 声明可以防止任何进一步的问题。

大多数时候只有一个 if 语句,这可能是以下原因之一:

  • 函数保护检查
  • 初始化选项
  • 可选处理分支

例子

void print (char * text)
{
    if (text == null) return; // guard check

    printf(text);
}

但是当你这样做时 if .. else if,这可能是以下原因之一:

  • 动态开关盒
  • 正在处理分叉
  • 处理一个处理参数

如果您的 if .. else if 涵盖了所有可能性,那么您的最后一个 if (...) 就不需要了,您可以将其删除,因为此时唯一可能的值就是涵盖的值按那个条件。

例子

int absolute_value (int n)
{
    if (n == 0)
    {
        return 0;
    }
    else if (n > 0)
    {
        return n;
    }
    else /* if (n < 0) */ // redundant check
    {
        return (n * (-1));
    }
}

并且在大多数这些原因中,可能有些东西不适合您 if .. else if 中的任何类别,因此需要在最后的 else 子句中处理它们,处理可以通过业务级流程、用户通知、内部错误机制等来完成

例子

#DEFINE SQRT_TWO   1.41421356237309504880
#DEFINE SQRT_THREE 1.73205080756887729352
#DEFINE SQRT_FIVE  2.23606797749978969641

double square_root (int n)
{
         if (n  > 5)   return sqrt((double)n);
    else if (n == 5)   return SQRT_FIVE;
    else if (n == 4)   return 2.0;
    else if (n == 3)   return SQRT_THREE;
    else if (n == 2)   return SQRT_TWO;
    else if (n == 1)   return 1.0;
    else if (n == 0)   return 0.0;
    else               return sqrt(-1); // error handling
}

最后的 else 子句与 JavaC++ 等语言中的一些其他内容非常相似,例如:

  • default switch 语句中的 case
  • catch(...) 在所有特定 catch 块之后出现
  • finally 在 try-catch 子句中

this question on boolean if/else if was closed as a duplicate. As well, there are many bad answers here as it relates to .

对于一个布尔值,只有两种情况。在 boolean 实例中,盲目地遵循 MISRA 的建议可能不好。代码,

if ( x == FALSE ) {
    // Normal action
} else if (x == TRUE ) {
    // Fail safe
}

应该重构为,

if ( x == FALSE ) {
    // Normal action
} else {
    // Fail safe
}

添加另一个 else 会增加循环复杂度,并使测试所有分支变得更加困难。一些代码可能 'safety related';即,不是可导致不安全事件的直接控制功能。在此代码中,最好在没有检测的情况下具有完整的可测试性。

对于真正的安全功能代码,分离案例以检测此代码中的错误并报告它可能是有意义的。尽管我认为在失败时记录 'x' 可以解决这两个问题。对于其他情况,这将使系统更难测试,并可能导致可用性降低,具体取决于第二个 'error handling' 操作是什么(请参阅调用 exit() 的其他答案)。


对于 non-booleans,可能存在无意义的范围。也就是说,它们可能是一些进入 DAC 的模拟变量。在这些情况下,if(x > 2) a; else if(x < -2) b; else c; 对于不应发送死区的情况等是有意义的。但是,这些类型的情况对于布尔值不存在。