用 else 子句终止 if... else if 结构的好处是什么?

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

If... else if 构造应该用 else 子句终止

例子一:

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

例二:

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 */
}

这个设计用于处理什么样的边缘情况?

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

20114 次浏览

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

规则14.10(必需) : 所有的 if... else if 构造都应该终止 加上 else 条款。

只要 if 语句后面跟着一个或多个,此规则就适用 Else if 语句; 最后的 else if后面应该跟一个 else 如果是简单的 if语句,那么 else 声明不需要包括在内。最终 else的要求 防御性编程 采取适当行动或载有适当的意见,说明为何没有 采取的行动。这是一致的要求,有一个 在 switch语句中的 final default子句 是一个简单的 if 语句:

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

而下面的代码演示了一个 ifelse if结构

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 */
}

在取代2004年版本的 MISRA-C: 2012年中,同样的规则存在,但编号为15.7是当前新项目的推荐版本。

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

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

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

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

if(condition_1 || condition_2 || ... condition_n)
{
//operation_1
}
else if(condition_1 || condition_2 || ... condition_n)
{
//operation_2
}
....
else
{
//default cause
}

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .

这样做是为了让代码更易于阅读,以便日后参考,并且让以后的评论者明白,最后一个 else处理的剩余案例,是 什么都别做案例,这样它们就不会在第一眼看到的时候被忽略。

这是一个很好的编程实践,使代码 可重复使用可扩展的

这相当于在每个开关中都需要一个默认情况。

这个额外的其他将减少你的程序的 代码覆盖率


在我的经验中移植 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 编码指南。目的是防御性编程,这个概念在关键任务编程中经常使用。

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

这有两个原因:

  • 自记录代码。如果你写了一个 else,但是留空它的意思是: “我肯定考虑过这种情况,当 ifelse if都不是真的”。

    在这里不写 else意味着: “要么我考虑了 ifelse if都不为真的场景,要么我完全忘了考虑它,而且我的代码中可能存在一个很大的 bug。”。

  • 停止失控代码。在任务关键型软件中,您需要编写可靠的程序,即使是那些极不可能的程序。这样你就可以看到

    if (mybool == TRUE)
    {
    }
    else if (mybool == FALSE)
    {
    }
    else
    {
    // handle error
    }
    

    这段代码对于 PC 程序员和计算机科学家来说是完全陌生的,但是它在任务关键型软件中非常有意义,因为它捕捉到了“ mybool”因为任何原因而被破坏的情况。

    从历史上看,您可能会担心由于 EMI/噪声而损坏 RAM 内存。这在今天已经不是什么大问题了。更有可能的是,内存损坏是由于代码中其他地方的错误造成的: 指向错误位置的指针、数组出界错误、堆栈溢出、代码失控等等。

    因此,大多数时候,当您在实现阶段编写错误时,这样的代码会回过头来给自己一记耳光。这意味着它也可以用作一种调试技术: 您正在编写的程序会告诉您什么时候编写了 bug。


剪辑

关于为什么每次 if之后不需要 else:

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

另外,如果在每个 if之后都写一个空的 else,那么代码就会完全混乱。

MISRA-C: 201215.7没有解释为什么不需要 else,它只是说:

注意: 简单的 if不需要最终的 else语句 声明。

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

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

这可能有利于下一个维护程序员的到来。他们不必搜索太远来判断代码是否正确。你可以算是 捕捉大象

就个人而言,它帮助我,因为它迫使我看到其他情况,并评估它。这可能是一个不可能的条件,在这种情况下,我可以抛出一个例外,因为违反了合同。它可能是良性的,在这种情况下,一条评论可能就足够了。

你的情况可能会有所不同。

基本原因可能是代码覆盖率和隐式 else: 如果条件不为真,代码将如何运行?对于真正的测试,您需要一些方法来确定您已经使用条件 false 进行了测试。如果您拥有的每个测试用例都经历了 If 子句,那么您的代码在现实世界中可能会因为您没有测试的条件而出现问题。

但是,有些条件可能与示例1类似,比如在纳税申报表上: “如果结果小于0,则输入0。”您仍然需要有一个条件为 false 的测试。

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

在大多数语言中,没有语法要求包含“ null”else语句(以及不必要的 {..}) ,在“简单的小程序”中也没有必要。但是真正的程序员不会编写“简单的小程序”,同样重要的是,他们不会编写一次性使用然后丢弃的程序。

当一个人写下一个 if/else:

if(something)
doSomething;
else
doSomethingElse;

这一切似乎很简单,人们甚至看不到增加 {..}的意义。

但是有一天,几个月后,会有其他的程序员(你永远不会犯这样的错误!)将需要“增强”程序,并将添加一个声明。

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

是的,它的格式不正确,但是项目中有一半的代码也是如此,而且所有的 #ifdef语句都把“ auto format”搞错了。当然,真正的代码要比这个例子复杂得多。

不幸的是(或者没有) ,我已经离开这类事情好几年了,所以我脑子里没有一个新的“真实”的例子——上面(显然)是人为的,有点做作。

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

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

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

我想对以前的答案加以补充,但也有部分自相矛盾的地方。虽然以类似于开关的方式使用 if-else 当然很常见,这种方式应该覆盖表达式的所有可考虑的值,但是并不能保证任何可能的条件范围都被完全覆盖。对于开关结构本身也可以这样说,因此需要使用一个 default 子句,它捕获所有剩余的值,并且可以(如果没有其他要求的话)用作断言保护。

这个问题本身就有一个很好的反例: 第二个条件根本与 x 无关(这就是为什么我经常更喜欢更灵活的基于 if 的变体而不是基于开关的变体)。从这个例子中可以很明显地看出,如果条件 A 得到满足,x 应该被设置为一个特定的值。如果不符合 A,则测试条件 B。如果满足,则 x 应该接收另一个值。如果既不满足 A 也不满足 B,则 x 应保持不变。

这里我们可以看到,应该使用一个空 else 分支来评论程序员对读者的意图。

另一方面,我不明白为什么一定要有一个 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 结构”,就会自相矛盾。

任何人都可以想象深嵌套如果其他树,请看这里的附注的丑陋。现在假设这个构造可以任意扩展到任何地方。那么在最后要求一个 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 结构”的含义

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

好吧,我的例子涉及到未定义行为,但是有时候有些人试图变得花哨,但是失败了,看看这个:

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,这不是 truefalse,但它可能会发生。我个人认为,这是由于人谁决定做一些花哨的问题,但额外的 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++等语言中的很少其他东西非常相似,例如:

  • Switch 语句中的 default大小写
  • 紧跟在所有特定 catch块之后的 catch(...)
  • Try-catch 子句中的 finally

因为 关于 < em > boolean if/else if 的问题是作为一个副本关闭的。同样,这里也有许多与 相关的错误答案。

对于 布尔型来说,只有两种情况。在 布尔型的例子中,盲目地遵循 MISRA 建议可能是不好的。密码,

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

应该重构为,

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

添加其他元素会增加循环复杂度,使得测试所有分支更加困难。有些代码可能是“安全相关”的; 也就是说,不是可能导致不安全事件的直接控制函数。在这段代码中,不使用检测通常具有完全的可测试性更好。

对于真正安全的函数代码,可能需要分离这些情况来检测代码中的错误并报告它。尽管我认为在故障上记录“ x”可以同时处理这两个问题。对于其他情况,它将使系统更难测试,并可能导致更低的可用性,这取决于第二个“错误处理”操作是什么(请参阅调用 exit()的其他答案)。


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