失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 可读代码编写炸鸡六 - 控制流尽量向前奔涌就好 不要分心

可读代码编写炸鸡六 - 控制流尽量向前奔涌就好 不要分心

时间:2024-01-28 09:08:48

相关推荐

可读代码编写炸鸡六 - 控制流尽量向前奔涌就好 不要分心

大家好,我是多选参数的一员 —— 大炮。

目前我还没想好该怎么写,多谢之前不嫌麻烦画的思维导图。

在上一篇作为引子的炸鸡中,我们知道接下来的可读代码的优化方向来到了开始接触代码逻辑上的优化的第二层。

而第二层分成如下几个方面:

控制流易读

拆分表达式

变量与可读性

所以很自然,本篇炸鸡是针对控制流易读这一方面写一些东西。

所以本篇炸鸡依照如图所示,分为两个大块来提供,针对于控制流的代码优化。

回复 「 控制流 」即可获得思维导图源文件

写在前头

首先我们得知道控制流这个概念是什么。控制流其实就是if/while/for这样的改变代码运行走向的语句。

那么使用控制流不当,就自然会产生一个问题:

代码逻辑走向变得复杂繁琐,让人需要大量的精力去追踪,从而产生思想包袱

所以我们需要做的就是:

让控制流更加地自然,让阅读者能够尽量从上至下阅读完,能有基本的理解,而不需要来来回回反复横跳来观看。

条件语句

众所周知,if/else算是程序员的老朋友了。值得一提的事,我的项目中,有些功能硬是用if/else分支支撑起来。

那么if/else分支容易造成什么问题?

条件语句参数

其实这个细节在看书前,我还真没有注意到,其实这个细节更适合放到第一层来讲。

我们试想一下,假设我们接收到了一些数据,需要判断字节数是否超出发送缓冲区长度,超出则将缓冲区数据发送,没有则将数据存入。

其中判断是否超出长度时,缓冲区长度通常会用一个常量MAX_BUFFER_LENGTH定义,那么条件语句应该是:

1ifreceiveByte<=MAX_BUFFER_LENGTHthen2...3end

还是:

1ifMAX_BUFFER_LENGTH<=receiveBytethen2...3end

回想一下,为了控制流语句更加自然,我们可以想象一下我们平常是怎么说一个判断的:

1接受的字节数比缓冲区长度少的话,我们就...

所以左边是变化的值,右边是尽量被比较的,固定的值

条件语句组织顺序

使用if/else/elseif这样的代码组织终究是很普遍的,那么多种条件下,会产生多行条件语句,那么这时候,怎么样给这些条件罗列下来比较方便阅读呢?

我们可以看一个例子?

1ifid~=ROOT_ID2andself:isInBlockList(id)3andid~=MGR_IDthen4...5...6...7...8...9...10elseifid==ROOT_IDthen11...12...13elseifnotidthen14...15...16...17...

例子中的省略号其实就代表了代码行数的多少,我们可以发现一开始就花精力其记住三个条件接着往下看代码,已经带着一定的思想包袱了。

所以简单先行。将简单的条件放在前头判断,减轻负担。

1ifid==ROOT_IDthen2...3elseifnotidthen4...5elseifself:isInBlockList(id)6andid~=MGR_IDthen7...8end

而且我们可以发现,由于条件语句顺序的对换,原本id ~= ROOT_ID and self:isInBlockList(id) and id ~= MGR_ID这样较大的条件语句也被缩减了。

但是如上的判断语句也可以这么修改。

1ifnotidthenreturnend2ifid==ROOT_IDthen3...4elseifself:isInBlockList(id)5andid~=MGR_IDthen6...7end

这便是又一种顺序:有错误或者非预期情况,放置在前头。

这样的写法通常就是用来降低代码出错的可能性。同时提前 return 还减少不必要的代码运行,也减少了后续条件语句的分支。

这样的写法还有一个帮助,便是减少代码嵌套。这个我们下文会提到,便不再赘述。

那么为什么不这么调整呢?

1ifnotidthen2...3elseifid==ROOT_IDthen4...5elseifself:isInBlockList(id)6andid~=MGR_IDthen7...8end

其实这样也是可以的,但是个人更建议将not id作为一种错误情况提前返回,而不是放在具体的条件分支中。

如果看过之前的炸鸡的朋友们应该对布尔型变量的命名有所印象,其中一条建议是:不要用否定意味的词来修饰,会导致意思很绕,反而增加阅读障碍。

放诸条件语句也是类似的,不仅影响理解,而且人对否定有更强烈的逆反心思,这样就不是自然的阅读顺序。

所以,条件语句组织顺序大致三种:

简单先行

错误先抛

正先否后

当然,当你写代码的时候,可以灵活选择这几个顺序,不用过于死板。

三目运算符 / do .. while / goto

因为这三者的内容不多,就放一起了。对他们的结论便是:

尽量少用吧。

三目

我们先说三目运算符。一般三目运算符可以替代简单的条件判断,从而简化代码。

1localret=byte<10andtrueorfalse

1intret=byte<10?true:false

重点就是简单。如果比较复杂的条件判断,用三目运算符是反而增加阅读难度的,并不能起到代码简化的作用。

1localret=(self:isInBlockList(id)andid~=MGR_ID)andprint(...)or....

这样的代码是变成了一行了,但在脑子里不还是拆成分支了,无疑是徒增烦恼。

do .. while

那么接下来是do .. while

这个语句不建议用有两个原因。第一个是因为顺序不够自然。要执行到底部,然后再看判断条件,然后再回头,这样其实比较奇怪。

第二个原因其实也是因为代码要先执行到底部,然后再看判断条件。因为我们知道,循环语句中的条件判断while(...)其实是起到了一种保护作用,让伤害循环体代码的数据不会被执行。

但是do .. while不管对错都要执行一遍,这样当循环体中接收的数据有问题,无疑是伤害代码运行的。

goto

目前我遇到的人中,谈goto必建议不要用。其实关键在于是否破坏了自然顺序,让人不是自上而下阅读,而是反复横跳地看。

1for(..)2{3if(!...){goto_exit}4}56_exit:7print("多选参数886")

可以看到,所示代码其实没有什么问题,也不会增加什么思想包袱。

但是这样的话就是破坏顺序了:

1_second:23_first:4...56for(..)7{8if(!...){goto_exit;}9if(...){goto_second;}10if(...){goto_first;}11}1213_exit:14printf("多选参数886");

所以还是少用吧。

嵌套

代码不可能都是一两个循环,条件判断就会解决的。

正是这些循环,条件判断的交织组合,造就了代码嵌套

代码嵌套很容易导致思想包袱,而且还是思想入栈。你的脑子会一直想着:

1在这个情况下,进入...,同时记住这个情况下,再进入...

像不像把你当前记着的条件入栈,但是你要知道,你的脑子不是计算机,没有保护现场。

那么为啥容易出现这种嵌套?

嵌套积累的原因

需求的变动。需求的变动。需求的变动。需求的变动。

这个是不少程序员深恶痛绝的东西,当周期截止时,突然告诉你

1朋友,需求改了。23辛苦辛苦加加班。45我先回去了,你们加油。

直接喷脏话,不走程序。所以一开始这样的代码:

1...2for_,kidinipairs(kids)do3ifkid==SELF_KIDthen4...5end6end

很容易就变成了:

1...2for_,kidinipairs(kids)do3ifkid==SELF_KIDthen4...5ifkid%1000~=0then6...7else8...9end10else11...12ifkid==0then...end13end14end

提前 return

我们可以拿上一篇炸鸡的例子开刀子,如下代码其实就是嵌套的一灾区。

1--QuestionSystem为一个类23localConfig=require"Config"4functionQuestionSystem:isEnd()5returnself.hasAnswerdCount==Config.MaxQuestionCount6end78--题目系统,提供下一题9functionQuestionSystem:giveNextQuestion(questionId,isAnswerCorrect)10localquestion={11isEnd=false,12maxAnswerTime=11,13nextQuestionId=0,14}1516localquestionConfig=Config.getConfById(questionId)17ifquestionConfig.nextType==NEXT_TYPE.ANY18or(isAnswerCorrect19andquestionConfig.nextType==NEXT_TYPE.RIGHT)then20ifnotself:isEnd()then21self._count=self._count+122question.isEnd=false23--随机一个24ifquestionConfig.nextQuestionId==-1then25question.nextQuestionId=math.random(1,questionConfig.maxNum)26else27question.nextQuestionId=questionConfig.nextQuestionId28end29question.maxAnswerTime=Config.getConfById(question.nextQuestionId).maxAnswerTime30returnquestion31else32question.isEnd=true33returnquestion34end35end36end

我们阅读到判断语句时,我饿们需要先 「 入栈 」一个判断

1questionConfig.nextType==NEXT_TYPE.ANY2or(isAnswerCorrectandquestionConfig.nextType==NEXT_TYPE.RIGHT)

当我们记住的时候,又遇到一个判断

1notself:isEnd()

再将这个判断记住,思维入栈,我们又遇到了一个:

1questionConfig.nextQuestionId==-1

好了,你的脑子里的思维栈已经是这样了:

但你发现,还有其他情况,你得将这些情况一个个出栈再入栈。一通操作后,如上所示的思维栈说不定又印象不深了。于是又要在读一遍,周而复始,套娃行为。

我们知道,函数调用就是一次入栈,我们把思维入栈就当做一个函数,而阅读这样的嵌套代码,等于不断地传入不同判断条件的不断调用此函数的递归操作。那么我们不要那么多次入栈,就少调用它就好了,也就是提前 return

对的,前文提到的条件判断利用提前 return 来减少判断分支,其实已经是在尽量避免嵌套。

所以我们可以尝试着将一些情况先提前 return:

1localquestionConfig=Config.getConfById(questionId)2localneedJumpForAnyway=(questionJumpCondition==QUESTION_JUMP_CONDITION_TYPE.Anyway)3localneedJumpForAnswerCorrect=(questionJumpCondition==QUESTION_JUMP_CONDITION_TYPE.RightandisAnswerCorrect)45if(notneedJumpForAnyway)and(notneedJumpForAnswerCorrect)thenreturnquestionend67ifself.isEnd()then8question.isEnd=true9returnquestion10end111213ifquestionConfig.nextQuestionId==-1then14question.nextQuestionId=math.random(1,questionConfig.maxNum)15else16question.nextQuestionId=questionConfig.nextQuestionId17end18question.maxAnswerTime=Config.getConfById(question.nextQuestionId).maxAnswerTime19returnquestion

可以对比之前的多嵌套代码,将一些特殊情况提前抛出,代码不仅清爽了不少,阅读起来思维入栈也不多。

而且这里已经使用了解释性变量来简化条件表达式,而这个内容,后几篇炸鸡会提到的,这里就看个效果图个乐。

循环中嵌套

嵌套的情况不光是if/else这样的嵌套,还有循环中的嵌套。例如前文的例子:

1...2for_,kidinipairs(kids)do3ifkid==SELF_KIDthen4...5ifkid%1000~=0then6...7else8...9end10else11...12ifkid==0then...end13end14end

解决这样的嵌套其实核心思想没有变,就是提前 return。

不过对于循环语句来说,挺多情况没法 return,那么就需要对于循环来说的提前 return

就好比continue。如下代码所示,利用continue在遇到特殊情况提前停止当前循环,进入下一轮循环。

这样也会给阅读者一个印象,需要continue的条件是不被这个代码需要的。

1for_,kidinipairs(kids)do2--lua没有continue,所以用这个词来模拟。3ifkid~=SELF_KIDorkid==0then_continueend4ifkid%1000~=0then5...6else7...8end9end

当然,提前 return 这样的方法有时候也能试用,例如从一个table找到指定的值:

1for_,kidinipairs(kids)do2ifkid==SELF_KIDandkid%1000~=0thenreturnkidend3end

当然,如果实在不能通过如上方式来简化,判断逻辑就是如此复杂该怎么办呢?

封装成函数即可

1functionisKidOk(kid)2ifkid~=SELF_KIDorkid==0thenreturnfalseend3ifkid%1000~=0then4returntrue5else6returnfalse7end8end910for_,kidinipairs(kids)do11--lua没有continue,所以用这个词来模拟。12ifnotisKidOk(kid)then_continue13else...end14end

总结

好了,说了这么多,也不知道多少人能看到这里,还是做一个小小的总结吧。

本篇炸鸡是可读代码编写的第二层的第一个方面 —— 控制流易读。而控制流易读的核心便是

1阅读起来自然,不需要重复回头看,少量的思维包袱。

所以围绕这个核心,提出了一些优化方法:

条件语句参数的顺序,左变化,右固定。

if/else 的条件放置顺序大致有三个讲究,简单先行,错误先抛,正先否后

do .. whilegoto还有三目运算符尽量少用。

为减少代码嵌套的副作用,导致思维入栈,尽量使用提前 return的思想。

好了,本篇炸鸡暂时到这里,谢谢各位的阅读,如果觉得本篇炸鸡不错的朋友,可以点个在看或者将此篇炸鸡转发给你的朋友,谢谢各位的支持。

再次感谢各位对多选参数的支持。

不甘于「本该如此」,「多选参数」值得关注

如果觉得《可读代码编写炸鸡六 - 控制流尽量向前奔涌就好 不要分心》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。