再写if-else,就把你消灭

发布者:EEWorld资讯最新更新时间:2023-12-28 来源: EEWORLD关键字:if-else 手机看文章 扫描二维码
随时随地手机看文章

翻开市面大部分编程教程,最早能够接触到的条件语句基本都是if-else。

 

作为高级编程语言都有的必备功能,if-else在嵌入式编程过程中几乎是必用。但任何东西都有限度,滥用它,只会让代码逐渐抽象、离谱,最后成为“屎山”。

 

接手项目的程序员叫苦不迭;忍无可忍的CTO对着满屏的if-else终于下定决心,下次逮着谁再写if-else,罚款1000块。

 

那么,为什么if-else这么不受待见,怎么干掉它,不滥用它?


付斌|作者

电子工程世界(ID:EEWorldbbs)|出品


 if-else的两宗罪 

 

实际上,我们并不是杜绝if-else,而是建议少用。

 

究其原因,一共有两点:一是影响程序的运行效率;二是影响代码的可读性,增大运维难度。

 

目前,大部分人的观点是if-else的分支预测(Branch Prediction)会降低执行效率。

 

CPU执行一条指令分为IF、ID、EX、WB四个阶段。分阶段执行(也就是pipeline流水线执行)会先给出一个预测结果,让流水线直接执行,执行对了则继续;执行错了,则退回去重新执行,直到对为止。

 

图片

 

比如说,在这样的代码中:

 

int a = 0;
a += 1;
a += 2;
a += 3;

 

CPU并非运行完a=0后执行a +=1,而是在运行a=0读执行后,马上运行a +=1的读执行。

 

图片

 

流水线执行的好处很多,但对if语句来说,就会开启分支预测,如果预测失败,就会影响执行时间。但未对数组排序前提下,分支预测大概率会失败,从而导致指令执行结束后重新读取下一条指令,无法发挥流水线效果。说白了,只有分支预测一直成功,CPU执行效率才会大幅提升。

 

图片

 

不过,需要强调的是,所有带有跳转结构的语句比如if、switch、for都会出现这种情况。而且,现代CPU性能远超过去,只有存在巨大量if-else情况才会影响性能,即便出现性能问题,也得在一个结构清洗的架构上分析性能是不是才能更好的定位,实在不行也可以把这段替换成汇编。

 

所以相比来说,导致程序运行效率下降并非if-else的最大原罪,而是影响可读性。

 

代码本质是给人看的,人得能看懂,从上到下扫一下大概就明白设计意图、思路就是好代码,这样的代码也是基本没有if-else的,如果还需要反复从上看到下,再从下看到上,这样的代码及时这次看懂隔一段时间也会忘记的。也就是“高内聚,低耦合”。

 

许多工程师在接手老项目时,时常会收到扑面而来的是,代码中可能会充斥着大量的 if/else ,嵌套 6、7 层,一个函数几百行,多层判断让人无从下手,绝望、愤怒、无奈喷涌而出,而且这样的惨案已经不止发生过一次。总结成一句话就是,看死人。

 

图片

 

这种代码风格早在几十年前就被国外所批判,并被称之为"箭头代码"(Arrow-code)。

 

图片

 

实际工作中,我们能见到一个方法包含10个、20个甚至更多的逻辑分支的情况。而更为致命的情况就是if-else的多层嵌套。


代码的多层嵌套拥有很大隐患,也给代码库增加了很多不必要的复杂性。

 

人脑一次只能处理几件不同的事情,因此当面临需要深入分析多个代码层级时,很容易忘记上一层的关键逻辑,导致一些不必要的错误。

 

还是那句话,代码是给人看的,我们的业务流程已经足够复杂了,多层嵌套还会进一步增加了其复杂度。

 

if (someConditionIsMet) {
   // ...
   // ...
   // ...
   // 接下来是 100 行代码
   // ...
   // ...
   // ...
   // 还有 100 行
   if (someOtherConditionIsMet) {
       // ...
       // ...
       // ...
       // 接下来是 100 行代码
       // ...
       // ...
       // ...
       if (yetAnotherConditionIsMet) {
           // ...
           // ...
           // ...
           // 接下来是 100 行代码
           // ...
           // ...
           // ...
           } else {
           // 现在,处理边缘情况
           }
       // ...
       // ...
       // ...
   } else {
       // 现在,处理边缘情况
       return someOtherResult;
   }
   // ...
   // ...
   // ...
} else {
// 现在,处理边缘情况
}
return someResult; 


遇到这种情况怎么办,不要慌,直接干掉if-else。

 

 怎么干掉if-else 

第一种方法:排非策略


优化前


 if (user && password) {
       // 逻辑处理
   } else {
       throw('用户名和密码不能为空!')
   }


优化后


if (!user || !password) return throw('用户名和密码不能为空!')
// 逻辑处理


第二种方法:三元运算符


三元运算符相比if-else来说,只需一行语句,代码简练精炼。

 

示例一


let allow = nullif(age >= 18){   allow = '通过'; } else {   allow = '拒绝'; }
// 优化后let allow = age >= 18 ? '通过' : '拒绝'


示例二

if (flag) {
 success();
} else {
 fail();
}
  
//优化后
flag ? success() : fail();


第三种方法:使用switch、key-value和Map

if (this.type === 'A') {
 this.handleA();
} else if (this.type === 'B') {
 this.handleB();
} else if (this.type === 'C') {
 this.handleC();
} else if (this.type === 'D') {
 this.handleD();
} else {
 this.handleE();
}

 

Switch显然更简单,而且,不同的条件分支之间没有嵌套,并且它们彼此独立,逻辑很清楚。不过,代码本身也会有点多。


switch(val){
   case 'A':
     handleA()
     break
   case 'B':
     handleB()
     break
   case 'C':
     handleC()
     break
   case 'D':
     handleD()
     break
 }

 

此时key- value和Map就是很好的方法。

let enums = {
 'A': handleA,
 'B': handleB,
 'C': handleC,
 'D': handleD,
 'E': handleE
}
function action(val){
 let handleType = enums[val]
 handleType()
}


let enums = new Map([
  ['A', handleA],
  ['B', handleB],
  ['C', handleC],
  ['D', handleD],
  ['E', handleE]
])
function action(val){
  let handleType = enums(val)
  handleType()
}


第四种方法:逻辑与运算符

 

有些时候我们可以使用逻辑与运算符来简化代码。

 

if( falg ){    someMethod()}
修改成:falg && someMethod();


第五种方法:使用 includes 处理多重条件


if( code === '202' || code === '203' || code === '204' ){    someMethod()}

修改成if( ['202','203','204'].includes(code) ){    someMethod()}


第六种方法:责任链模式和策略模式

 

责任链模式是实现了类似“流水线”结构的逐级处理,通常是一条链式结构,将“抽象处理者”的不同实现串联起来。策略模式的目的是将算法的使用与定义解耦,能够实现根据规则路由到不同策略类进行处理。

 

当然,并不是说用if-else就很low,用设计模式就高大上,二者擅长场景不同,if-else足以满足大部分日常需求的开发,且简单、灵活、可靠,而设计模式则是为了更简洁、拓展性好、性能更优、可读性更好等。


 抛弃else吧,你会打开新世界 


当然,无论哪种重构方法,都只是优化。归结起来,最简单的方法就是在写代码之处,抛弃else。抛弃 else 就可以减少嵌套层数,有效降低代码复杂度。让代码更简单,结构更清晰,更容易维护。

 

如果我们抛弃 if-else 块中的 else 并优先处理这块逻辑。先处理小的边界情况,如果必要的话提前返回;否则,将主流程保留在函数的最外层。

 

if (!someConditionIsMet) {  // 首先处理那个边缘情况  return someResultOrNothing;}

// 主流程可以继续,不需要额外的保护块// ...// ...// ...// 再加 100 行代码// ...// ...// ...// 还有 100 行

return someResult;


同样的思路也可以应用于处理多个边缘情况:


if (!someConditionIsMet) {  // 首先处理那个边缘情况  return someResultOrNothing;}

if (!someOtherConditionIsMet) {  // 首先处理那个边缘情况  return someResultOrNothing;}

if (!yetAnotherConditionIsMet) {  // 首先处理那个边缘情况  return someResultOrNothing;}

// 主流程可以继续,不需要额外的保护块// ...// ...// ...// 再加 100 行代码// ...// ...// ...// 还有 100 行

return someResult;

 

由于边界情况在函数的顶部得到了处理,开发者在后续添加新代码时不太可能忽略这些情况,从而降低了引入错误的风险。处理边缘情况并早期返回可以减少不必要的计算,从而可能提高代码的执行效率。

 

简化的代码结构更容易被同事理解,从而使代码审查过程更顺畅,减少潜在错误和不良实践传播。

 

当然,也有工程师认为,不要过度关注if-else的数量和层数,而是要关注语义是否清晰,单纯减少if-else但又让代码更难阅读反而是画蛇添足的。乱优化if-else没什么好处,应该优化的是本身条件的分割,把流理清楚后if-else比啥都清晰。

 

所以最好的做法是写一篇详细文档,从最原始的数学模型开始,表明采取何种计算策略,策略如何推倒,然后给整个方法加上注释附上文档地址,并且在每个分支的地方加上注释指明对应到文档中哪个公式。

 

总结起来,就是一句话:没有对与错,而是要看场景,任何时候,只要能让代码更易读和维护,就成功了。



参考文献

[1] 非典型技术宅:为什么建议少用if语句,不是运行效率!.2021.5.4.https://mp.weixin.qq.com/s/CHASap6dHJP_Cn2nL6aMiA
[2] CSDN:现在开始,把代码里的 ‘else’ 丢掉!”.2023.10.04.https://mp.weixin.qq.com/s/rRlV_sresmxGDFt8RMFraQ
[3] 知乎:优化代码中大量的 if/else ,你有什么方案?
[4] 前端有道:代码中大量的if/else,你有什么优化方案?.2021.5.21.https://mp.weixin.qq.com/s/LY-6IwOqXiBiN_TY0aq_6Q
[5] 阿里云开发者:如何优化你的if-else?来试试“责任树模式”.2021.1.26.https://mp.weixin.qq.com/s/Wib0Ly45te00HMUnIG-tbg
关键字:if-else 引用地址:再写if-else,就把你消灭

上一篇:临港新片区基金公司联合行业翘楚睿赛德科技 举办2023RT-Thread开发者大会
下一篇:嵌入式开发的转变将如何影响未来计算

小广播
最新嵌入式文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved