编程的智慧.docx

上传人:b****2 文档编号:17572232 上传时间:2023-07-26 格式:DOCX 页数:36 大小:38.07KB
下载 相关 举报
编程的智慧.docx_第1页
第1页 / 共36页
编程的智慧.docx_第2页
第2页 / 共36页
编程的智慧.docx_第3页
第3页 / 共36页
编程的智慧.docx_第4页
第4页 / 共36页
编程的智慧.docx_第5页
第5页 / 共36页
编程的智慧.docx_第6页
第6页 / 共36页
编程的智慧.docx_第7页
第7页 / 共36页
编程的智慧.docx_第8页
第8页 / 共36页
编程的智慧.docx_第9页
第9页 / 共36页
编程的智慧.docx_第10页
第10页 / 共36页
编程的智慧.docx_第11页
第11页 / 共36页
编程的智慧.docx_第12页
第12页 / 共36页
编程的智慧.docx_第13页
第13页 / 共36页
编程的智慧.docx_第14页
第14页 / 共36页
编程的智慧.docx_第15页
第15页 / 共36页
编程的智慧.docx_第16页
第16页 / 共36页
编程的智慧.docx_第17页
第17页 / 共36页
编程的智慧.docx_第18页
第18页 / 共36页
编程的智慧.docx_第19页
第19页 / 共36页
编程的智慧.docx_第20页
第20页 / 共36页
亲,该文档总共36页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

编程的智慧.docx

《编程的智慧.docx》由会员分享,可在线阅读,更多相关《编程的智慧.docx(36页珍藏版)》请在冰点文库上搜索。

编程的智慧.docx

编程的智慧

编程的智慧

----来自王垠的总结

编程是一种创造性的工作,是一门艺术。

精通任何一门艺术,都需要很多的练习和领悟,所以这里提出的“智慧”,并不是号称一天瘦十斤的减肥药,它并不能代替你自己的勤奋。

然而由于软件行业喜欢标新立异,喜欢把简单的事情搞复杂,我希望这些文字能给迷惑中的人们指出一些正确的方向,让他们少走一些弯路,基本做到一分耕耘一分收获。

反复推敲代码

既然“天才是百分之一的灵感,百分之九十九的汗水”,那我先来谈谈这汗水的部分吧。

有人问我,提高编程水平最有效的办法是什么?

我想了很久,终于发现最有效的办法,其实是反反复复地修改和推敲代码。

在IU的时候,由于DanFriedman的严格教导,我们以写出冗长复杂的代码为耻。

如果你代码多写了几行,这老顽童就会大笑,说:

“当年我解决这个问题,只写了5行代码,你回去再想想吧……”当然,有时候他只是夸张一下,故意刺激你的,其实没有人能只用5行代码完成。

然而这种提炼代码,减少冗余的习惯,却由此深入了我的骨髓。

有些人喜欢炫耀自己写了多少多少万行的代码,仿佛代码的数量是衡量编程水平的标准。

然而,如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不可能提高编程水平的。

你会制造出越来越多平庸甚至糟糕的代码。

在这种意义上,很多人所谓的“工作经验”,跟他代码的质量,其实不一定成正比。

如果有几十年的工作经验,却从来不回头去提炼和反思自己的代码,那么他也许还不如一个只有一两年经验,却喜欢反复推敲,仔细领悟的人。

有位文豪说得好:

“看一个作家的水平,不是看他发表了多少文字,而要看他的废纸篓里扔掉了多少。

”我觉得同样的理论适用于编程。

好的程序员,他们删掉的代码,比留下来的还要多很多。

如果你看见一个人写了很多代码,却没有删掉多少,那他的代码一定有很多垃圾。

就像文学作品一样,代码是不可能一蹴而就的。

灵感似乎总是零零星星,陆陆续续到来的。

任何人都不可能一笔呵成,就算再厉害的程序员,也需要经过一段时间,才能发现最简单优雅的写法。

有时候你反复提炼一段代码,觉得到了顶峰,没法再改进了,可是过了几个月再回头来看,又发现好多可以改进和简化的地方。

这跟写文章一模一样,回头看几个月或者几年前写的东西,你总能发现一些改进。

所以如果反复提炼代码已经不再有进展,那么你可以暂时把它放下。

过几个星期或者几个月再回头来看,也许就有焕然一新的灵感。

这样反反复复很多次之后,你就积累起了灵感和智慧,从而能够在遇到新问题的时候直接朝正确,或者接近正确的方向前进。

写优雅的代码

人们都讨厌“面条代码”(spaghetticode),因为它就像面条一样绕来绕去,没法理清头绪。

那么优雅的代码一般是什么形状的呢?

经过多年的观察,我发现优雅的代码,在形状上有一些明显的特征。

如果我们忽略具体的内容,从大体结构上来看,优雅的代码看起来就像是一些整整齐齐,套在一起的盒子。

如果跟整理房间做一个类比,就很容易理解。

如果你把所有物品都丢在一个很大的抽屉里,那么它们就会全都混在一起。

你就很难整理,很难迅速的找到需要的东西。

但是如果你在抽屉里再放几个小盒子,把物品分门别类放进去,那么它们就不会到处乱跑,你就可以比较容易的找到和管理它们。

优雅的代码的另一个特征是,它的逻辑大体上看起来,是枝丫分明的树状结构(tree)。

这是因为程序所做的几乎一切事情,都是信息的传递和分支。

你可以把代码看成是一个电路,电流经过导线,分流或者汇合。

如果你是这样思考的,你的代码里就会比较少出现只有一个分支的if语句,它看起来就会像这个样子:

if(...){

if(...){

...

}else{

...

}

}elseif(...){

...

}else{

...

}

注意到了吗?

在我的代码里面,if语句几乎总是有两个分支。

它们有可能嵌套,有多层的缩进,而且else分支里面有可能出现少量重复的代码。

然而这样的结构,逻辑却非常严密和清晰。

在后面我会告诉你为什么if语句最好有两个分支。

写模块化的代码

有些人吵着闹着要让程序“模块化”,结果他们的做法是把代码分部到多个文件和目录里面,然后把这些目录或者文件叫做“module”。

他们甚至把这些目录分放在不同的VCSrepo里面。

结果这样的作法并没有带来合作的流畅,而是带来了许多的麻烦。

这是因为他们其实并不理解什么叫做“模块”,肤浅的把代码切割开来,分放在不同的位置,其实非但不能达到模块化的目的,而且制造了不必要的麻烦。

真正的模块化,并不是文本意义上的,而是逻辑意义上的。

一个模块应该像一个电路芯片,它有定义良好的输入和输出。

实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。

每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以你其实根本不需要把代码分开在多个文件或者目录里面,同样可以完成代码的模块化。

我可以把代码全都写在同一个文件里,却仍然是非常模块化的代码。

想要达到很好的模块化,你需要做到以下几点:

∙避免写太长的函数。

如果发现函数太大了,就应该把它拆分成几个更小的。

通常我写的函数长度都不超过40行。

对比一下,一般笔记本电脑屏幕所能容纳的代码行数是50行。

我可以一目了然的看见一个40行的函数,而不需要滚屏。

只有40行而不是50行的原因是,我的眼球不转的话,最大的视角只看得到40行代码。

如果我看代码不转眼球的话,我就能把整片代码完整的映射到我的视觉神经里,这样就算忽然闭上眼睛,我也能看得见这段代码。

我发现闭上眼睛的时候,大脑能够更加有效地处理代码,你能想象这段代码可以变成什么其它的形状。

40行并不是一个很大的限制,因为函数里面比较复杂的部分,往往早就被我提取出去,做成了更小的函数,然后从原来的函数里面调用。

∙制造小的工具函数。

如果你仔细观察代码,就会发现其实里面有很多的重复。

这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。

有些帮助函数也许就只有两行,然而它们却能大大简化主要函数里面的逻辑。

有些人不喜欢使用小的函数,因为他们想避免函数调用的开销,结果他们写出几百行之大的函数。

这是一种过时的观念。

现代的编译器都能自动的把小的函数内联(inline)到调用它的地方,所以根本不产生函数调用,也就不会产生任何多余的开销。

同样的一些人,也爱使用宏(macro)来代替小函数,这也是一种过时的观念。

在早期的C语言编译器里,只有宏是静态“内联”的,所以他们使用宏,其实是为了达到内联的目的。

然而能否内联,其实并不是宏与函数的根本区别。

宏与函数有着巨大的区别(这个我以后再讲),应该尽量避免使用宏。

为了内联而使用宏,其实是滥用了宏,这会引起各种各样的麻烦,比如使程序难以理解,难以调试,容易出错等等。

∙每个函数只做一件简单的事情。

有些人喜欢制造一些“通用”的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来“选择”这个函数所要做的事情。

比如,你也许写出这样的函数:

∙voidfoo(){

∙if(getOS().equals("MacOS")){

∙a();

∙}else{

∙b();

∙}

∙c();

∙if(getOS().equals("MacOS")){

∙d();

∙}else{

∙e();

∙}

∙}

写这个函数的人,根据系统是否为“MacOS”来做不同的事情。

你可以看出这个函数里,其实只有c()是两种系统共有的,而其它的a(),b(),d(),e()都属于不同的分支。

这种“复用”其实是有害的。

如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。

其实,上面这个函数可以改写成两个函数:

voidfooMacOS(){

a();

c();

d();

}

voidfooOther(){

b();

c();

e();

}

如果你发现两件事情大部分内容相同,只有少数不同,多半时候你可以把相同的部分提取出去,做成一个辅助函数。

比如,如果你有个函数是这样:

voidfoo(){

a();

b()

c();

if(getOS().equals("MacOS")){

d();

}else{

e();

}

}

其中a(),b(),c()都是一样的,只有d()和e()根据系统有所不同。

那么你可以把a(),b(),c()提取出去:

voidpreFoo(){

a();

b()

c();

然后制造两个函数:

voidfooMacOS(){

preFoo();

d();

}

voidfooOther(){

preFoo();

e();

}

这样一来,我们既共享了代码,又做到了每个函数只做一件简单的事情。

这样的代码,逻辑就更加清晰。

∙避免使用全局变量和类成员(classmember)来传递信息,尽量使用局部变量和参数。

有些人写代码,经常用类成员来传递信息,就像这样:

∙classA{

∙Stringx;

∙voidfindX(){

∙...

∙x=...;

∙}

∙voidfoo(){

∙findX();

∙...

∙print(x);

∙}

∙}

首先,他使用findX(),把一个值写入成员x。

然后,使用x的值。

这样,x就变成了findX和print之间的数据通道。

由于x属于classA,这样程序就失去了模块化的结构。

由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。

findX和foo不再能够离开classA而存在,而且由于类成员还有可能被其他代码改变,代码变得难以理解,难以确保正确性。

如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个class,而且更加容易理解,不易出错:

StringfindX(){

...

x=...;

returnx;

}

voidfoo(){

intx=findX();

print(x);

}

写可读的代码

有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。

注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。

而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。

修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石。

实际上,真正优雅可读的代码,是几乎不需要注释的。

如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。

其实,程序语言相比自然语言,是更加强大而严谨的,它其实具有自然语言最主要的元素:

主语,谓语,宾语,名词,动词,如果,那么,否则,是,不是,……所以如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。

有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一些违反直觉的作法。

这时候你可以使用很短注释,说明为什么要写成那奇怪的样子。

这样的情况应该少出现,否则这意味着整个代码的设计都有问题。

如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。

所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要:

1.使用有意义的函数和变量名字。

如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。

比如:

2.//putelephant1intofridge2

3.put(elephant1,fridge2);

由于我的函数名put,加上两个有意义的变量名elephant1和fridge2,已经说明了这是在干什么(把大象放进冰箱),所以上面那句注释完全没有必要。

4.局部变量应该尽量接近使用它的地方。

有些人喜欢在函数最开头定义很多局部变量,然后在下面很远的地方使用它,就像这个样子:

5.voidfoo(){

6.intindex=...;

7....

8....

9.bar(index);

10....

11.}

由于这中间都没有使用过index,也没有改变过它所依赖的数据,所以这个变量定义,其实可以挪到接近使用它的地方:

voidfoo(){

...

...

intindex=...;

bar(index);

...

}

这样读者看到bar(index),不需要向上看很远就能发现index是如何算出来的。

而且这种短距离,可以加强读者对于这里的“计算顺序”的理解。

否则如果index在顶上,读者可能会怀疑,它其实保存了某种会变化的数据,或者它后来又被修改过。

如果index放在下面,读者就清楚的知道,index并不是保存了什么可变的值,而且它算出来之后就没变过。

如果你看透了局部变量的本质——它们就是电路里的导线,那你就能更好的理解近距离的好处。

变量定义离用的地方越近,导线的长度就越短。

你不需要摸着一根导线,绕来绕去找很远,就能发现接收它的端口,这样的电路就更容易理解。

12.局部变量名字应该简短。

这貌似跟第一点相冲突,简短的变量名怎么可能有意义呢?

注意我这里说的是局部变量,因为它们处于局部,再加上第2点已经把它放到离使用位置尽量近的地方,所以根据上下文你就会容易知道它的意思:

比如,你有一个局部变量,表示一个操作是否成功:

booleansuccessInDeleteFile=deleteFile("foo.txt");

if(successInDeleteFile){

...

}else{

...

}

这个局部变量successInDeleteFile大可不必这么啰嗦。

因为它只用过一次,而且用它的地方就在下面一行,所以读者可以轻松发现它是deleteFile返回的结果。

如果你把它改名为success,其实读者根据一点上下文,也知道它表示"successindeleteFile"。

所以你可以把它改成这样:

booleansuccess=deleteFile("foo.txt");

if(success){

...

}else{

...

}

这样的写法不但没漏掉任何有用的语义信息,而且更加易读。

successInDeleteFile这种"camelCase",如果超过了三个单词连在一起,其实是很碍眼的东西,所以如果你能用一个单词表示同样的意义,那当然更好。

13.不要重用局部变量。

很多人写代码不喜欢定义新的局部变量,而喜欢“重用”同一个局部变量,通过反复对它们进行赋值,来表示完全不同意思。

比如这样写:

14.Stringmsg;

15.if(...){

16.msg="succeed";

17.log.info(msg);

18.}else{

19.msg="failed";

20.log.info(msg);

21.}

虽然这样在逻辑上是没有问题的,然而却不易理解,容易混淆。

变量msg两次被赋值,表示完全不同的两个值。

它们立即被log.info使用,没有传递到其它地方去。

这种赋值的做法,把局部变量的作用域不必要的增大,让人以为它可能在将来改变,也许会在其它地方被使用。

更好的做法,其实是定义两个变量:

if(...){

Stringmsg="succeed";

log.info(msg);

}else{

Stringmsg="failed";

log.info(msg);

}

由于这两个msg变量的作用域仅限于它们所处的if语句分支,你可以很清楚的看到这两个msg被使用的范围,而且知道它们之间没有任何关系。

22.把复杂的逻辑提取出去,做成“帮助函数”。

有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。

如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。

由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。

举一个例子:

23....

24.//putelephant1intofridge2

25.openDoor(fridge2);

26.if(elephant1.alive()){

27....

28.}else{

29....

30.}

31.closeDoor(fridge2);

32....

如果你把这片代码提出去定义成一个函数:

voidput(Elephantelephant,Fridgefridge){

openDoor(fridge);

if(elephant.alive()){

...

}else{

...

}

closeDoor(fridge);

}

这样原来的代码就可以改成:

...

put(elephant1,fridge2);

...

更加清晰,而且注释也没必要了。

33.把复杂的表达式提取出去,做成中间变量。

有些人听说“函数式编程”是个好东西,也不理解它的真正含义,就在代码里使用大量嵌套的函数。

像这样:

34.Pizzapizza=makePizza(crust(salt(),butter()),

35.topping(onion(),tomato(),sausage()));

这样的代码一行太长,而且嵌套太多,不容易看清楚。

其实训练有素的函数式程序员,都知道中间变量的好处,不会盲目的使用嵌套的函数。

他们会把这代码变成这样:

Crustcrust=crust(salt(),butter());

Toppingtopping=topping(onion(),tomato(),sausage());

Pizzapizza=makePizza(crust,topping);

这样写,不但有效地控制了单行代码的长度,而且由于引入的中间变量具有“意义”,步骤清晰,变得很容易理解。

36.在合理的地方换行。

对于绝大部分的程序语言,代码的逻辑是和空白字符无关的,所以你可以在几乎任何地方换行,你也可以不换行。

这样的语言设计,是一个好东西,因为它给了程序员自由控制自己代码格式的能力。

然而,它也引起了一些问题,因为很多人不知道如何合理的换行。

有些人喜欢利用IDE的自动换行机制,编辑之后用一个热键把整个代码重新格式化一遍,IDE就会把超过行宽限制的代码自动折行。

可是这种自动这行,往往没有根据代码的逻辑来进行,不能帮助理解代码。

自动换行之后可能产生这样的代码:

if(someLongCondition1()&&someLongCondition2()&&someLongCondition3()&&

someLongCondition4()){

...

}

由于someLongCondition4()超过了行宽限制,被编辑器自动换到了下面一行。

虽然满足了行宽限制,换行的位置却是相当任意的,它并不能帮助人理解这代码的逻辑。

这几个boolean表达式,全都用&&连接,所以它们其实处于平等的地位。

为了表达这一点,当需要折行的时候,你应该把每一个表达式都放到新的一行,就像这个样子:

if(someLongCondition1()&&

someLongCondition2()&&

someLongCondition3()&&

someLongCondition4()){

...

}

这样每一个条件都对齐,里面的逻辑就很清楚了。

再举个例子:

log.info("failedtofindfile{}forcommand{},withexception{}",file,command,

exception);

这行因为太长,被自动折行成这个样子。

file,command和exception本来是同一类东西,却有两个留在了第一行,最后一个被折到第二行。

它就不如手动换行成这个样子:

log.info("failedtofindfile{}forcommand{},withexception{}",

file,command,exception);

把格式字符串单独放在一行,而把它的参数一并放在另外一行,这样逻辑就更加清晰。

为了避免IDE把这些手动调整好的换行弄乱,很多IDE(比如IntelliJ)的自动格式化设定里都有“保留原来的换行符”的设定。

如果你发现IDE的换行不符合逻辑,你可以修改这些设定,然后在某些地方保留你自己的手动换行。

说到这里,我必须警告你,这里所说的“不需注释,让代码自己解释自己”,并不是说要让代码看起来像某种自然语言。

有个叫Chai的JavaScript测试工具,可以让你这样写代码:

expect(foo).to.be.a('string');

expect(foo).to.equal('bar');

expect(foo).to.have.length(3);

expect(tea).to.have.property('flavors').with.length(3);

这种做法是极其错误的。

程序语言本来就比自然语言简单清晰,这种写法让它看起来像自然语言的样子,反而变得复杂难懂了。

写简单的代码

程序语言都喜欢标新立异,提供这样那样的“特性”,然而有些特性其实并不是什么好东西。

很多特性都经不起时间的考验,最后带来的麻烦,比解决的问题还多。

很多人盲目的追求“短小”和“精悍”,或者为了显示自己头脑聪明,学得快,所以喜欢利用语言里的一些特殊构造,写出过于“聪明”,难以理解的代码。

并不是语言提供什么,你就一定要把它用上的。

实际上你只需要其中很小的一部分功能,就能写出优秀的代码。

我一向反对“充分利用”程序语言里的所有特性。

实际上,我心目中有一套最好的构造。

不管语言提供了多么“神奇”的,“新”的特性,我基本都只用经过千锤百炼,我觉得值得信奈的那一套。

现在针对一些有问题的语言特性,我介绍一些我自己使用的代码规范,并且讲解一下为什么它们能让代码更简单。

∙避免使用自增减表达式(i++,++i,i--,--i)。

这种自增减操作表达式其实是历史遗留的设计失误。

它们含义蹊跷,非常容易弄错。

它们把读和写这两种完全不同的操作,混淆缠绕在一起,把语义搞得乌七八糟。

含有它们的表达式,结果可能取决于求值顺序,所以它可能在某种编译器下能正确运行,换一个编译器就出现离奇的错误。

其实这两个表达式完全可以分解成两步,把读和写分开:

一步更新i的值,另外一步使用i的值。

比如,如果你想写foo(i++),你完全可以把它拆成intt=i;i+=1;foo(t);。

如果你想写foo(++i),可以拆成i+=1;foo(i);拆开之后的代码,含义完全一致,却清晰很多。

到底更新是在取值之前还是之后,一目了然。

有人也许以为i++或者++i的效率比拆开之后要高,这只是一种错觉。

这些代码经过基本的编译器优化之后,生成的机器代码是完全没有区别的。

自增减表达式只有在两种情况下才可以安全的使用。

一种是在for循环的update部分,比如for(inti=0;i

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 经管营销 > 生产经营管理

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2