If-else的故事
前言
当我在大学机房的电脑上敲下人生中的第一个 if-else
时,我还不知道日后会和他有什么样的故事,只是单纯的觉得这个东西好简单啊!不就是要不这样 — 要不那样
,编程也没什么难度嘛。从此 if-else
就像是我的好朋友,总是出现在我的代码中,他真的是我的好助手,帮助我理清代码逻辑,实现各种各样功能。
直到今天他依然陪伴着我,我身边时不时会有反对他的声音,甚至在互联网上能搜索到大量的文章,在讨论”如何去掉if-else
”,可见 if-else
在2023年的今天已经不能满足coder的需求了。但我相信99%的coder无法去掉代码中的if-else
,因为他就是那样简单且强大,他是连接计算机与人类大脑,最便捷的桥梁。
if-else 源
在20世纪中,世界上第一台计算机ENIAC诞生了。它的计算能力确实很强大,但是如何让它计算人们想要的东西呢?
起初科学家发明了打孔纸带,提前预制好不同功能的纸带(有点像封装函数哈),让计算机读取这条纸带是否有孔来给计算机“编程”。这时候的编程呢,就是组合这些纸带,最后让计算机读取,以此来实现最终的计算目标。科学家每天都需要制作超长的纸带来满足计算机,这也太累了吧!他们可是最会“偷懒”的人怎么能把时间消耗在打洞上呢!
还是在在20世纪中,汇编语言就出现了。此时有孔和没孔用1和0来表示,但是正常人很难记忆这么多的的01数字,随着功能越来越多,01数字的排序也越来越复杂,所以对这些数字组成的功能用助记符来表示,也可以说封装一下,这样便于记忆和使用。例如,用ADD表示0000 0001
,在使用时你一看ADD就知道是加的意思,不用去数里面有几个0有几个1。
SUB(0000 0010)
、MOVE(0000 0011)
、JMP(0000 0100)
、CMP(0000 0101)
汇编的if-else
故事背景先介绍到这里,那么汇编语言如何表示要不这样 — 要不那样
?
这里有三段C语言代码和对应的汇编代码,先阅读一下图片上方的C语言代码,都是最简单的if-else。
看完之后我们来分析一下图片下方的汇编代码,重点是红框内的内容,这里主要分析第一段代码,重点是cmpl
和jle
1 | call __main //调用main方法 |
执行完jle .L2
后,如果跳转到L2标签则会跳过 leaq .LC0(%rip),%rcx
和call puts
这两行代码,直接去执行.LC1(%rip),%rcx
,现在注意第一段汇编代码的开头的LC0,它里面存放的的就是“big”这个字符串,”small”存放在LC1。
现在翻译一下这段汇编代码,如果 a ≤ 3 直接去L2,输出small,否则先输出big,再输出small,这样通过cmpl
和jle
的配合就完成了一次if-else
。但是对照上面的C语言发现,输出结果虽然一样,但代码中明明写的是 a > 0,到这里却变成了jle(Jump less equal)
?这其实是因为编译器在对我们的源码进行编译时会进行优化处理,在保证语义正确的前提下,它会选择最优的执行方式。
CMP 和 PSW
jle(Jump less equal)
这个还是比较好理解的“小于等于就跳”,那么CMP要如何理解
计算机要如何比较两个数大小呢?实际上就是对两个值做减法,结果只会有三种:正数、负数和零。我们记录一下这三种结果,通过它们就可以知道两个数的比较结果。
在CPU中有专门记录这种数值的帮手“程序状态字PSW(Program Status Word)“,CPU中控制单元会根据ALU(Arithmetic and Logic Unit)的计算结果修改程序状态字PSW(Program Status Word),JMP 命令会根据PSW决定是顺序执行,还是跳转到标签的地址
到这里我们终于知道汇编语言如何表示要不这样 — 要不那样
,那么它又是如何变成if-else的?
当你用汇编语言去实现一个很复杂的功能时,你会发现屏幕上会有非常多的像.L2这样的标签,一会跳去这个标签,一会跳去那个标签,这样格式的代码不利于后续的维护,于是就诞生了高级编程语言。
if-else通常是在高级语言中出现的关键字,所谓高级语言,就是更易于人理解的语言。通过ADD、SUB等助记符的进一步组合封装,就可以实现我们的高级语言。回过头再看看上面那三段代码,同样的功能,汇编代码确实要长不少。
switch “同气连枝”
在编程语言发展的过程中,人们发现如果要判断的条件过多,if-else
写起来就很麻烦,于是switch
出现了。
switch
可以根据一个整数索引值进行多重分支(multiway branching) 表达,在处理具有多种可能的结果时,这种语句特别有用,它不仅提高了C代码的可读性,而且通过使用跳转表(jump table)这种数据结构使得实现更加高效。
在switch语句中,case
后面的值和:
后面的代码地址会被保存在跳转表中。使用跳转表会避免CPU执行过多的cmp操作。下图是switch语句示例的汇编代码和相应的跳转表。
不同语言中的switch可能有不同的特性,比如不止对整数索引值进行多重分支处理,还能处理字符串等其他类型的值,但是底层逻辑还是和C语言一样的。
“放弃纸带编程”
为什么要消除代码中的if-else
呢,或许就像当初放弃纸带编程一样。
今天的程序也是越写越复杂,代码量越来越多。数不清的if-else,写的时候很爽,二次维护却很折磨人,一不小心就会漏掉某些条件,所以大家经常喊“消除if-else”!
业务开发(CRUD)
在日常业务开发时你可能会用到枚举类和策略模式对if-else进行优化。在我的实践中发现,只是单纯的用各种方法替换if-else,优化效果并不明显,方法内部嵌套过深或者枚举类和设计模式运用的不好反而会让代码更难阅读。
毕竟if-else带来的问题只是不方便阅读,我认为要写出简洁、可维护性高的代码,更好的方法是结合CleanCode中说的小技巧,整体提升代码质量。
游戏开发
你现在要做一款游戏,尝试创建一个“超人”对象,他可以跑跳飞,并且有3个技能,你要如何编写代码,让这个超人释放技能呢?用if-else试试吧
1 | // status == 0 |
看起来还不错,但是此时如果还需要为这个角色增加一个状态,应该如何做呢?最快的选择当然是 增加一个 if status == 3,游戏复杂度真的远超我们想象,如果在开发时真的像我这样做写到一半估计就写不下去了,非常多的条件要判断,非常多的优先级要判断。
在LOL游戏中“解脱者·塞拉斯“和“破败之王·佛耶戈“就是比较特殊的角色人物,某种情况下他们可以复制其他角色的技能,这一特性也让他们携带了各种各样的BUG,这两个人物有这么多BUG的原因,就是因为他们可以复制其他人物的技能,不同的人物的技能触发条件,优先级都不同。这意味着这两个人物的技能触发情况要和所有其他角色适配,这个工作量可想而知。这时候还要用if-else吗?
其实在游戏领域最常见的就是“状态机”模型,通过预先设置好对象状态、触发条件、动作等一些列属性,在使用时可以隐藏触发状态变换的细节,只关心对象当前的动作,简化游戏设计。
总结
我不知道if-else还会陪伴我走多远,计算机没有魔法,写好你的条件,if-else它会告诉你答案,要不这样 — 要不那样
。