learun开发社区 - 力软.net/java快速开发平台官方论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 3810|回复: 23

编程的智慧

  [复制链接]

9

主题

9

帖子

56

积分

新手上路

Rank: 1

积分
56
发表于 2018-12-21 14:46:01 | 显示全部楼层 |阅读模式
编程是一种创造性的工作,是一门艺术。精通任何一门艺术,都需要很多的练习和领悟,所以这里提出的“智慧”,并不是号称一天瘦十斤的减肥药,它并不能代替你自己的勤奋。然而由于软件行业喜欢标新立异,喜欢把简单的事情搞复杂,我希望这些文字能给迷惑中的人们指出一些正确的方向,让他们少走一些弯路,基本做到一分耕耘一分收获。
反复推敲代码既然“天才是百分之一的灵感,百分之九十九的汗水”,那我先来谈谈这汗水的部分吧。有人问我,提高编程水平最有效的办法是什么?我想了很久,终于发现最有效的办法,其实是反反复复地修改和推敲代码。
在IU的时候,由于Dan Friedman的严格教导,我们以写出冗长复杂的代码为耻。如果你代码�多写了几行,这老顽童就会大笑,说:“当年我解决这个问题,只写了5行代码,你回去再想想吧……” 当然,有时候他只是夸张一下,故意刺激你的,其实没有人能只用5行代码完成。然而这种提炼代码,减少冗余的习惯,却由此深入了我的骨髓。
有些人喜欢炫耀自己写了多少多少万行的代码,仿佛代码的数量是衡量编程水平的标准。然而,如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不可能提高编程水平的。你会制造出越来越多平庸甚至糟糕的代码。在这种意义上,很多人所谓的“工作经验”,跟他代码的质量,其实不一定成正比。如果有几十年的工作经验,却从来不回头去提炼和反思自己的代码,那么他也许还不如一个只有一两年经验,却喜欢反复推敲,仔细领悟的人。
有位文豪说得好:“看一个作家的水平,不是看他发表了多少文字,而要看他的废纸篓里扔掉了多少。” 我觉得同样的理论适用于编程。好的程序员,他们删掉的代码,比留下来的还要多很多。如果你看见一个人写了很多代码,却没有删掉多少,那他的代码一定有很多垃圾。
就像文学作品一样,代码是不可能一蹴而就的。灵感似乎总是零零星星,陆陆续续到来的。任何人都不可能一笔呵成,就算再厉害的程序员,也需要经过一段时间,才能发现最简单优雅的写法。有时候你反复提炼一段代码,觉得到了顶峰,没法再改进了,可是过了几个月再回头来看,又发现好多可以改进和简化的地方。这跟写文章一模一样,回头看几个月或者几年前写的东西,你总能发现一些改进。
所以如果反复提炼代码已经不再有进展,那么你可以暂时把它放下。过几个星期或者几个月再回头来看,也许就有焕然一新的灵感。这样反反复复很多次之后,你就积累起了灵感和智慧,从而能够在遇到新问题的时候直接朝正确,或者接近正确的方向前进。
写优雅的代码人们都讨厌“面条代码”(spaghetti code),因为它就像面条一样绕来绕去,没法理清头绪。那么优雅的代码一般是什么形状的呢?经过多年的观察,我发现优雅的代码,在形状上有一些明显的特征。
如果我们忽略具体的内容,从大体结构上来看,优雅的代码看起来就像是一些整整齐齐,套在一起的盒子。如果跟整理房间做一个类比,就很容易理解。如果你把所有物品都丢在一个很大的抽屉里,那么它们就会全都混在一起。你就很难整理,很难迅速的找到需要的东西。但是如果你在抽屉里再放几个小盒子,把物品分门别类放进去,那么它们就不会到处乱跑,你就可以比较容易的找到和管理它们。
优雅的代码的另一个特征是,它的逻辑大体上看起来,是枝丫分明的树状结构(tree)。这是因为程序所做的几乎一切事情,都是信息的传递和分支。你可以把代码看成是一个电路,电流经过导线,分流或者汇合。如果你是这样思考的,你的代码里就会比较少出现只有一个分支的if语句,它看起来就会像这个样子:
  1. <span class="hljs-keyword">if</span> (...) {
  2.   <span class="hljs-keyword">if</span> (...) {
  3.     ...
  4.   } <span class="hljs-keyword">else</span> {
  5.     ...
  6.   }
  7. } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (...) {
  8.   ...
  9. } <span class="hljs-keyword">else</span> {
  10.   ...
  11. }
复制代码

注意到了吗?在我的代码里面,if语句几乎总是有两个分支。它们有可能嵌套,有多层的缩进,而且else分支里面有可能出现少量重复的代码。然而这样的结构,逻辑却非常严密和清晰。在后面我会告诉你为什么if语句最好有两个分支。
写模块化的代码有些人吵着闹着要让程序“模块化”,结果他们的做法是把代码分部到多个文件和目录里面,然后把这些目录或者文件叫做“module”。他们甚至把这些目录分放在不同的VCS repo里面。结果这样的作法并没有带来合作的流畅,而是带来了许多的麻烦。这是因为他们其实并不理解什么叫做“模块”,肤浅的把代码切割开来,分放在不同的位置,其实非但不能达到模块化的目的,而且制造了不必要的麻烦。
真正的模块化,并不是文本意义上的,而是逻辑意义上的。一个模块应该像一个电路芯片,它有定义良好的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以你其实根本不需要把代码分开在多个文件或者目录里面,同样可以完成代码的模块化。我可以把代码全都写在同一个文件里,却仍然是非常模块化的代码。
想要达到很好的模块化,你需要做到以下几点:
  • 避免写太长的函数。如果发现函数太大了,就应该把它拆分成几个更小的。通常我写的函数长度都不超过40行。对比一下,一般笔记本电脑屏幕所能容纳的代码行数是50行。我可以一目了然的看见一个40行的函数,而不需要滚屏。只有40行而不是50行的原因是,我的眼球不转的话,最大的视角只看得到40行代码。
    如果我看代码不转眼球的话,我就能把整片代码完整的映射到我的视觉神经里,这样就算忽然闭上眼睛,我也能看得见这段代码。我发现闭上眼睛的时候,大脑能够更加有效地处理代码,你能想象这段代码可以变成什么其它的形状。40行并不是一个很大的限制,因为函数里面比较复杂的部分,往往早就被我提取出去,做成了更小的函数,然后从原来的函数里面调用。
  • 制造小的工具函数。如果你仔细观察代码,就会发现其实里面有很多的重复。这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。有些帮助函数也许就只有两行,然而它们却能大大简化主要函数里面的逻辑。
    有些人不喜欢使用小的函数,因为他们想避免函数调用的开销,结果他们写出几百行之大的函数。这是一种过时的观念。现代的编译器都能自动的把小的函数内联(inline)到调用它的地方,所以根本不产生函数调用,也就不会产生任何多余的开销。
    同样的一些人,也爱使用宏(macro)来代替小函数,这也是一种过时的观念。在早期的C语言编译器里,只有宏是静态“内联”的,所以他们使用宏,其实是为了达到内联的目的。然而能否内联,其实并不是宏与函数的根本区别。宏与函数有着巨大的区别(这个我以后再讲),应该尽量避免使用宏。为了内联而使用宏,其实是滥用了宏,这会引起各种各样的麻烦,比如使程序难以理解,难以调试,容易出错等等。
  • 每个函数只做一件简单的事情。有些人喜欢制造一些“通用”的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来“选择”这个函数所要做的事情。比如,你也许写出这样的函数:
    1. <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span> </span>{
    2.   <span class="hljs-keyword">if</span> (getOS().equals(<span class="hljs-string">"MacOS"</span>)) {
    3.     a();
    4.   } <span class="hljs-keyword">else</span> {
    5.     b();
    6.   }
    7.   c();
    8.   <span class="hljs-keyword">if</span> (getOS().equals(<span class="hljs-string">"MacOS"</span>)) {
    9.     d();
    10.   } <span class="hljs-keyword">else</span> {
    11.     e();
    12.   }
    13. }
    复制代码

    写这个函数的人,根据系统是否为“MacOS”来做不同的事情。你可以看出这个函数里,其实只有c()是两种系统共有的,而其它的a(), b(), d(), e()都属于不同的分支。
    这种“复用”其实是有害的。如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。其实,上面这个函数可以改写成两个函数:
    1. <pre class="hljs cpp"><code class="cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">fooMacOS</span><span class="hljs-params">()</span> </span>{
    2.   a();
    3.   c();
    4.   d();
    5. }
    6. </code></pre>
    7. <p>和</p>
    8. <pre class="hljs cpp"><code class="cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">fooOther</span><span class="hljs-params">()</span> </span>{
    9.   b();
    10.   c();
    11.   e();
    12. }</code></pre>
    复制代码

    如果你发现两件事情大部分内容相同,只有少数不同,多半时候你可以把相同的部分提取出去,做成一个辅助函数。比如,如果你有个函数是这样:
    1. <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span> </span>{
    2.   a();
    3.   b()
    4.   c();
    5.   <span class="hljs-keyword">if</span> (getOS().equals(<span class="hljs-string">"MacOS"</span>)) {
    6.     d();
    7.   } <span class="hljs-keyword">else</span> {
    8.     e();
    9.   }
    10. }
    复制代码

    其中a(),b(),c()都是一样的,只有d()和e()根据系统有所不同。那么你可以把a(),b(),c()提取出去:
    1. <pre class="hljs cpp"><code class="cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">preFoo</span><span class="hljs-params">()</span> </span>{
    2.   a();
    3.   b()
    4.   c();
    5. </code></pre>
    6. <p>然后制造两个函数:</p>
    7. <pre class="hljs cpp"><code class="cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">fooMacOS</span><span class="hljs-params">()</span> </span>{
    8.   preFoo();
    9.   d();
    10. }
    11. </code></pre>
    12. <p>和</p>
    13. <pre class="hljs cpp"><code class="cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">fooOther</span><span class="hljs-params">()</span> </span>{
    14.   preFoo();
    15.   e();
    16. }</code></pre>
    复制代码

    这样一来,我们既共享了代码,又做到了每个函数只做一件简单的事情。这样的代码,逻辑就更加清晰。
  • 避免使用全局变量和类成员(class member)来传递信息,尽量使用局部变量和参数。有些人写代码,经常用类成员来传递信息,就像这样:
    1. <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">A</span> {</span>
    2.   String x;

    3.   <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">findX</span><span class="hljs-params">()</span> </span>{
    4.      ...
    5.      x = ...;
    6.   }

    7.   <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span> </span>{
    8.     findX();
    9.     ...
    10.     print(x);
    11.   }
    12. }
    复制代码

    首先,他使用findX(),把一个值写入成员x。然后,使用x的值。这样,x就变成了findX和print之间的数据通道。由于x属于class A,这样程序就失去了模块化的结构。由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。findX和foo不再能够离开class A而存在,而且由于类成员还有可能被其他代码改变,代码变得难以理解,难以确保正确性。
    如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个class,而且更加容易理解,不易出错:
    1. <span class="hljs-function">String <span class="hljs-title">findX</span><span class="hljs-params">()</span> </span>{
    2.    ...
    3.    x = ...;
    4.    <span class="hljs-keyword">return</span> x;
    5. }
    6. <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span> </span>{
    7.   String x = findX();
    8.   print(x);
    9. }
    复制代码


写可读的代码有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石。
实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。其实,程序语言相比自然语言,是更加强大而严谨的,它其实具有自然语言最主要的元素:主语,谓语,宾语,名词,动词,如果,那么,否则,是,不是,…… 所以如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。
有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一些违反直觉的作法。这时候你可以使用很短注释,说明为什么要写成那奇怪的样子。这样的情况应该少出现,否则这意味着整个代码的设计都有问题。
如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要:
  • 使用有意义的函数和变量名字。如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。比如:
    1. <span class="hljs-comment">// put elephant1 into fridge2</span>
    2. put(elephant1, fridge2);
    复制代码

    由于我的函数名put,加上两个有意义的变量名elephant1和fridge2,已经说明了这是在干什么(把大象放进冰箱),所以上面那句注释完全没有必要。
  • 局部变量应该尽量接近使用它的地方。有些人喜欢在函数最开头定义很多局部变量,然后在下面很远的地方使用它,就像这个样子:
    1. <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span> </span>{
    2.   <span class="hljs-keyword">int</span> index = ...;
    3.   ...
    4.   ...
    5.   bar(index);
    6.   ...
    7. }
    复制代码

    由于这中间都没有使用过index,也没有改变过它所依赖的数据,所以这个变量定义,其实可以挪到接近使用它的地方:
    1. <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span> </span>{
    2.   ...
    3.   ...
    4.   <span class="hljs-keyword">int</span> index = ...;
    5.   bar(index);
    6.   ...
    7. }
    复制代码

    这样读者看到bar(index),不需要向上看很远就能发现index是如何算出来的。而且这种短距离,可以加强读者对于这里的“计算顺序”的理解。否则如果index在顶上,读者可能会怀疑,它其实保存了某种会变化的数据,或者它后来又被修改过。如果index放在下面,读者就清楚的知道,index并不是保存了什么可变的值,而且它算出来之后就没变过。
    如果你看透了局部变量的本质——它们就是电路里的导线,那你就能更好的理解近距离的好处。变量定义离用的地方越近,导线的长度就越短。你不需要摸着一根导线,绕来绕去找很远,就能发现接收它的端口,这样的电路就更容易理解。
  • 局部变量名字应该简短。这貌似跟第一点相冲突,简短的变量名怎么可能有意义呢?注意我这里说的是局部变量,因为它们处于局部,再加上第2点已经把它放到离使用位置尽量近的地方,所以根据上下文你就会容易知道它的意思:
    比如,你有一个局部变量,表示一个操作是否成功:
    1. <span class="hljs-keyword">boolean</span> successInDeleteFile = deleteFile(<span class="hljs-string">"foo.txt"</span>);
    2. <span class="hljs-keyword">if</span> (successInDeleteFile) {
    3.   ...
    4. } <span class="hljs-keyword">else</span> {
    5.   ...
    6. }
    复制代码

    这个局部变量successInDeleteFile大可不必这么啰嗦。因为它只用过一次,而且用它的地方就在下面一行,所以读者可以轻松发现它是deleteFile返回的结果。如果你把它改名为success,其实读者根据一点上下文,也知道它表示"success in deleteFile"。所以你可以把它改成这样:
    1. <span class="hljs-keyword">boolean</span> success = deleteFile(<span class="hljs-string">"foo.txt"</span>);
    2. <span class="hljs-keyword">if</span> (success) {
    3.   ...
    4. } <span class="hljs-keyword">else</span> {
    5.   ...
    6. }
    复制代码

    这样的写法不但没漏掉任何有用的语义信息,而且更加易读。successInDeleteFile这种“camelCase”,如果超过了三个单词连在一起,其实是很碍眼的东西。所以如果你能用一个单词表示同样的意义,那当然更好。
  • 不要重用局部变量。很多人写代码不喜欢定义新的局部变量,而喜欢“重用”同一个局部变量,通过反复对它们进行赋值,来表示完全不同意思。比如这样写:
    1. String msg;
    2. <span class="hljs-keyword">if</span> (...) {
    3.   msg = <span class="hljs-string">"succeed"</span>;
    4.   <span class="hljs-built_in">log</span>.info(msg);
    5. } <span class="hljs-keyword">else</span> {
    6.   msg = <span class="hljs-string">"failed"</span>;
    7.   <span class="hljs-built_in">log</span>.info(msg);
    8. }
    复制代码

    虽然这样在逻辑上是没有问题的,然而却不易理解,容易混淆。变量msg两次被赋值,表示完全不同的两个值。它们立即被log.info使用,没有传递到其它地方去。这种赋值的做法,把局部变量的作用域不必要的增大,让人以为�它可能在将来改变,也许会在其它地方被使用。更好的做法,其实是定义两个变量:
    1. <span class="hljs-keyword">if</span> (...) {
    2.   String msg = <span class="hljs-string">"succeed"</span>;
    3.   <span class="hljs-built_in">log</span>.info(msg);
    4. } <span class="hljs-keyword">else</span> {
    5.   String msg = <span class="hljs-string">"failed"</span>;
    6.   <span class="hljs-built_in">log</span>.info(msg);
    7. }
    复制代码

    由于这两个msg变量的作用域仅限于它们所处的if语句分支,你可以很清楚的看到这两个msg被使用的范围,而且知道它们之间没有任何关系。
  • 把复杂的逻辑提取出去,做成“帮助函数”。有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。举一个例子:
    1. ...
    2. <span class="hljs-comment">// put elephant1 into fridge2</span>
    3. openDoor(fridge2);
    4. <span class="hljs-keyword">if</span> (�elephant1.�alive()) {
    5.   ...
    6. } <span class="hljs-keyword">else</span> {
    7.    ...
    8. }
    9. closeDoor(fridge2);
    10. ...
    复制代码

    如果你把这片代码提出去定义成一个函数:
    1. <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">put</span><span class="hljs-params">(Elephant elephant, Fridge fridge)</span> </span>{
    2.   openDoor(fridge);
    3.   <span class="hljs-keyword">if</span> (�elephant.alive()) {
    4.     ...
    5.   } <span class="hljs-keyword">else</span> {
    6.      ...
    7.   }
    8.   closeDoor(fridge);
    9. }
    复制代码

    这样原来的代码就可以改成:
    1. ...
    2. put(elephant1, fridge2);
    3. ...
    复制代码

    更加清晰,而且注释也没必要了。
  • 把复杂的表达式提取出去,做成中间变量。有些人听说“函数式编程”是个好东西,也不理解它的真正含义,就在代码里大量使用嵌套的函数。像这样:
    1. Pizza pizza = makePizza(crust(salt(), butter()),
    2.    topping(onion(), tomato(), sausage()));
    复制代码

    这样的代码一行太长,而且嵌套太多,不容易看清楚。其实训练有素的函数式程序员,都知道中间变量的好处,不会盲目的使用嵌套的函数。他们会把这代码变成这样:
    1. Crust crust = crust(salt(), butter());
    2. Topping topping = topping(onion(), tomato(), sausage());
    3. Pizza pizza = makePizza(crust, topping);
    复制代码

    这样写,不但有效地控制了单行代码的长度,而且由于引入的中间变量具有“意义”,步骤清晰,变得很容易理解。
  • 在合理的地方换行。对于绝大部分的程序语言,代码的逻辑是和空白字符无关的,所以你可以在几乎任何地方换行,你也可以不换行。这样的语言设计是个好东西,因为它给了程序员自由控制自己代码格式的能力。然而,它也引起了一些问题,因为很多人不知道如何合理的换行。

有些人喜欢利用IDE的自动换行机制,编辑之后用一个热键把整个代码重新格式化一遍,IDE就会把超过行宽限制的代码自动折行。可是这种自动这行,往往没有根据代码的逻辑来进行,不能帮助理解代码。自动换行之后可能产生这样的代码:
  1. <span class="hljs-keyword">if</span> (someLongCondition1() && someLongCondition2() && someLongCondition3() &&
  2.   someLongCondition4()) {
  3.   ...
  4. }
复制代码

由于someLongCondition4()超过了行宽限制,被编辑器自动换到了下面一行。虽然满足了行宽限制,换行的位置却是相当任意的,它并不能帮助人理解这代码的逻辑。这几个boolean表达式,全都用&&连接,所以它们其实处于平等的地位。为了表达这一点,当需要折行的时候,你应该把每一个表达式都放到新的一行,就像这个样子:
  1. <span class="hljs-keyword">if</span> (someLongCondition1() &&
  2.     someLongCondition2() &&
  3.     someLongCondition3() &&
  4.     someLongCondition4()) {
  5.   ...
  6. }
复制代码

这样每一个条件都对齐,里面的逻辑就很清楚了。再举个例子:
  1. <span class="hljs-selector-tag">log</span><span class="hljs-selector-class">.info</span>("<span class="hljs-selector-tag">failed</span> <span class="hljs-selector-tag">to</span> <span class="hljs-selector-tag">find</span> <span class="hljs-selector-tag">file</span> {} <span class="hljs-selector-tag">for</span> <span class="hljs-selector-tag">command</span> {}, <span class="hljs-selector-tag">with</span> <span class="hljs-selector-tag">exception</span> {}", <span class="hljs-selector-tag">file</span>, <span class="hljs-selector-tag">command</span>,
  2.   <span class="hljs-selector-tag">exception</span>);
复制代码

这行因为太长,被自动折行成这个样子。file,command和exception本来是同一类东西,却有两个留在了第一行,最后一个被折到第二行。它就不如手动换行成这个样子:
  1. <span class="hljs-selector-tag">log</span><span class="hljs-selector-class">.info</span>("<span class="hljs-selector-tag">failed</span> <span class="hljs-selector-tag">to</span> <span class="hljs-selector-tag">find</span> <span class="hljs-selector-tag">file</span> {} <span class="hljs-selector-tag">for</span> <span class="hljs-selector-tag">command</span> {}, <span class="hljs-selector-tag">with</span> <span class="hljs-selector-tag">exception</span> {}",
  2.   <span class="hljs-selector-tag">file</span>, <span class="hljs-selector-tag">command</span>, <span class="hljs-selector-tag">exception</span>);
复制代码

把格式字符串单独放在一行,而把它的参数一并放在另外一行,这样逻辑就更加清晰。
为了避免IDE把这些手动调整好的换行弄乱,很多IDE(比如IntelliJ)的自动格式化设定里都有“保留原来的换行符”的设定。如果你发现IDE的换行不符合逻辑,你可以修改这些设定,然后在某些地方保留你自己的手动换行。
说到这里,我必须警告你,这里所说的“不需注释,让代码自己解释自己”,并不是说要让代码看起来像某种自然语言。有个叫Chai的JavaScript测试工具,可以让你这样写代码:
  1. expect(foo).to.be.a(<span class="hljs-string">'string'</span>);
  2. expect(foo).to.equal(<span class="hljs-string">'bar'</span>);
  3. expect(foo).to.have.length(<span class="hljs-number">3</span>);
  4. expect(tea).to.have.property(<span class="hljs-string">'flavors'</span>).<span class="hljs-keyword">with</span>.length(<span class="hljs-number">3</span>);
复制代码

这种做法是极其错误的。程序语言本来就比自然语言简单清晰,这种写法让它看起来像自然语言的样子,反而变得复杂难懂了。
写简单的代码程序语言都喜欢标新立异,提供这样那样的“特性”,然而有些特性其实并不是什么好东西。很多特性都经不起时间的考验,最后带来的麻烦,比解决的问题还多。很多人盲目的追求“短小”和“精悍”,或者为了显示自己头脑聪明,学得快,所以喜欢利用语言里的一些特殊构造,写出过于“聪明”,难以理解的代码。
并不是语言提供什么,你就一定要把它用上的。实际上你只需要其中很小的一部分功能,就能写出优秀的代码。我一向反对“充分利用”程序语言里的所有特性。实际上,我心目中有一套最好的构造。不管语言提供了多么“神奇”的,“新”的特性,我基本都只用经过千锤百炼,我觉得值得信赖的那一套。
现在针对一些有问题的语言特性,我介绍一些我自己使用的代码规范,并且讲解一下为什么它们能让代码更简单。
  • 避免使用自增减表达式(i++,++i,i--,--i)。这种自增减操作表达式其实是历史遗留的设计失误。它们含义蹊跷,非常容易弄错。它们把读和写这两种完全不同的操作,混淆缠绕在一起,把语义搞得乌七八糟。含有它们的表达式,结果可能取决于求值顺序,所以它可能在某种编译器下能正确运行,换一个编译器就出现离奇的错误。
    其实这两个表达式完全可以分解成两步,把读和写分开:一步更新i的值,另外一步使用i的值。比如,如果你想写foo(i++),你完全可以把它拆成int t = i; i += 1; foo(t);。如果你想写foo(++i),可以拆成i += 1; foo(i); 拆开之后的代码,含义完全一致,却清晰很多。到底更新是在取值之前还是之后,一目了然。
    有人也许以为i++或者++i的效率比拆开之后要高,这只是一种错觉。这些代码经过基本的编译器优化之后,生成的机器代码是完全没有区别的。自增减表达式只有在两种情况下才可以安全的使用。一种是在for循环的update部分,比如for(int i = 0; i < 5; i++)。另一种情况是写成单独的一行,比如i++;。这两种情况是完全没有歧义的。你需要避免其它的情况,比如用在复杂的表达式里面,比如foo(i++),foo(++i) + foo(i),…… 没有人应该知道,或者去追究这些是什么意思。永远不要省略花括号。很多语言允许你在某种情况下省略掉花括号,比如C,Java都允许你在if语句里面只有一句话的时候省略掉花括号:
    1. <span class="hljs-keyword">if</span> (...)
    2.   action1();
    复制代码

    咋一看少打了两个字,多好。可是这其实经常引起奇怪的问题。比如,你后来想要加一句话action2()到这个if里面,于是你就把代码改成:
    1. <span class="hljs-keyword">if</span> (...)
    2.   action1();
    3.   action2();
    复制代码

    为了美观,你很小心的使用了action1()的缩进。咋一看它们是在一起的,所以你下意识里以为它们只会在if的条件为真的时候执行,然而action2()却其实在if外面,它会被无条件的执行。我把这种现象叫做“光学幻觉”(optical illusion),理论上每个程序员都应该发现这个错误,然而实际上却容易被忽视。
    那么你问,谁会这么傻,我在加入action2()的时候加上花括号不就行了?可是从设计的角度来看,这样其实并不是合理的作法。首先,也许你以后又想把action2()去掉,这样你为了样式一致,又得把花括号拿掉,烦不烦啊?其次,这使得代码样式不一致,有的if有花括号,有的又没有。况且,你为什么需要记住这个规则?如果你不问三七二十一,只要是if-else语句,把花括号全都打上,就可以想都不用想了,就当C和Java没提供给你这个特殊写法。这样就可以保持完全的一致性,减少不必要的思考。
    有人可能会说,全都打上花括号,只有一句话也打上,多碍眼啊?然而经过实行这种编码规范几年之后,我并没有发现这种写法更加碍眼,反而由于花括号的存在,使得代码界限明确,让我的眼睛负担更小了。
  • 合理使用括号,不要盲目依赖操作符优先级。利用操作符的优先级来减少括号,对于1 + 2 * 3这样常见的算数表达式,是没问题的。然而有些人如此的仇恨括号,以至于他们会写出2 << 7 - 2 * 3这样的表达式,而完全不用括号。
    这里的问题,在于移位操作<<的优先级,是很多人不熟悉,而且是违反常理的。由于x << 1相当于把x乘以2,很多人误以为这个表达式相当于(2 << 7) - (2 * 3),所以等于250。然而实际上<<的优先级比加法+还要低,所以这表达式其实相当于2 << (7 - 2 * 3),所以等于4!
    解决这个问题的办法,不是要每个人去把操作符优先级表给硬背下来,而是合理的加入括号。比如上面的例子,最好直接加上括号写成2 << (7 - 2 * 3)。虽然没有括号也表示同样的意思,但是加上括号就更加清晰,读者不再需要死记<<的优先级就能理解代码。
  • 避免使用continue和break。循环语句(for,while)里面出现return是没问题的,然而如果你使用了continue或者break,就会让循环的逻辑和终止条件变得复杂,难以确保正确。
    出现continue或者break的原因,往往是对循环的逻辑没有想清楚。如果你考虑周全了,应该是几乎不需要continue或者break的。如果你的循环里出现了continue或者break,你就应该考虑改写这个循环。改写循环的办法有多种:

    • 如果出现了continue,你往往只需要把continue的条件反向,就可以消除continue。
    • 如果出现了break,你往往可以把break的条件,合并到循环头部的终止条件里,从而去掉break。
    • 有时候你可以把break替换成return,从而去掉break。
    • 如果以上都失败了,你也许可以把循环里面复杂的部分提取出来,做成函数调用,之后continue或者break就可以去掉了。
    下面我对这些情况举一些例子。
    情况1:下面这段代码里面有一个continue:
    1. List<<span class="hljs-built_in">String</span>> goodNames = <span class="hljs-keyword">new</span> ArrayList<>();
    2. <span class="hljs-keyword">for</span> (<span class="hljs-built_in">String</span> name: names) {
    3.   <span class="hljs-keyword">if</span> (name.contains(<span class="hljs-string">"bad"</span>)) {
    4.     <span class="hljs-keyword">continue</span>;
    5.   }
    6.   goodNames.add(name);
    7.   ...
    8. }  
    复制代码

    它说:“如果name含有'bad'这个词,跳过后面的循环代码……” 注意,这是一种“负面”的描述,它不是在告诉你什么时候“做”一件事,而是在告诉你什么时候“不做”一件事。为了知道它到底在干什么,你必须搞清楚continue会导致哪些语句被跳过了,然后脑子里把逻辑反个向,你才能知道它到底想做什么。这就是为什么含有continue和break的循环不容易理解,它们依靠“控制流”来描述“不做什么”,“跳过什么”,结果到最后你也没搞清楚它到底“要做什么”。
    其实,我们只需要把continue的条件反向,这段代码就可以很容易的被转换成等价的,不含continue的代码:
    1. List<<span class="hljs-built_in">String</span>> goodNames = <span class="hljs-keyword">new</span> ArrayList<>();
    2. <span class="hljs-keyword">for</span> (<span class="hljs-built_in">String</span> name: names) {
    3.   <span class="hljs-keyword">if</span> (!name.contains(<span class="hljs-string">"bad"</span>)) {
    4.     goodNames.add(name);
    5.     ...
    6.   }
    7. }  
    复制代码

    goodNames.add(name);和它之后的代码全部被放到了if里面,多了一层缩进,然而continue却没有了。你再读这段代码,就会发现更加清晰。因为它是一种更加“正面”地描述。它说:“在name不含有'bad'这个词的时候,把它加到goodNames的链表里面……”
    情况2:for和while头部都有一个循环的“终止条件”,那本来应该是这个循环唯一的退出条件。如果你在循环中间有break,它其实给这个循环增加了一个退出条件。你往往只需要把这个条件合并到循环头部,就可以去掉break。
    比如下面这段代码:
    1. <span class="hljs-keyword">while</span> (condition1) {
    2.   ...
    3.   <span class="hljs-keyword">if</span> (condition2) {
    4.     <span class="hljs-built_in">break</span>;
    5.   }
    6. }
    复制代码

    当condition成立的时候,break会退出循环。其实你只需要把condition2反转之后,放到while头部的终止条件,就可以去掉这种break语句。改写后的代码如下:
    1. <span class="hljs-keyword">while</span> (condition1 && !condition2) {
    2.   ...
    3. }
    复制代码

    这种情况表面上貌似只适用于break出现在循环开头或者末尾的时候,然而其实大部分时候,break都可以通过某种方式,移动到循环的开头或者末尾。具体的例子我暂时没有,等出现的时候再加进来。
    情况3:很多break退出循环之后,其实接下来就是一个return。这种break往往可以直接换成return。比如下面这个例子:
    1. <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">hasBadName</span><span class="hljs-params">(List<String> names)</span> </span>{
    2.     <span class="hljs-keyword">boolean</span> result = <span class="hljs-keyword">false</span>;

    3.     <span class="hljs-keyword">for</span> (String name: names) {
    4.         <span class="hljs-keyword">if</span> (name.contains(<span class="hljs-string">"bad"</span>)) {
    5.             result = <span class="hljs-keyword">true</span>;
    6.             <span class="hljs-keyword">break</span>;
    7.         }
    8.     }
    9.     <span class="hljs-keyword">return</span> result;
    10. }
    复制代码

    这个函数检查names链表里是否存在一个名字,包含“bad”这个词。它的循环里包含一个break语句。这个函数可以被改写成:
    1. <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">hasBadName</span><span class="hljs-params">(List<String> names)</span> </span>{
    2.     <span class="hljs-keyword">for</span> (String name: names) {
    3.         <span class="hljs-keyword">if</span> (name.contains(<span class="hljs-string">"bad"</span>)) {
    4.             <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    5.         }
    6.     }
    7.     <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    8. }
    复制代码

    改进后的代码,在name里面含有“bad”的时候,直接用return true返回,而不是对result变量赋值,break出去,最后才返回。如果循环结束了还没有return,那就返回false,表示没有找到这样的名字。使用return来代替break,这样break语句和result这个变量,都一并被消除掉了。
    我曾经见过很多其他使用continue和break的例子,几乎无一例外的可以被消除掉,变换后的代码变得清晰很多。我的经验是,99%的break和continue,都可以通过替换成return语句,或者翻转if条件的方式来消除掉。剩下的1%含有复杂的逻辑,但也可以通过提取一个帮助函数来消除掉。修改之后的代码变得容易理解,容易确保正确。
写直观的代码我写代码有一条重要的原则:如果有更加直接,更加清晰的写法,就选择它,即使它看起来更长,更笨,也一样选择它。比如,Unix命令行有一种“巧妙”的写法是这样:
command1 && command2 && command3由于Shell语言的逻辑操作a && b具有“短路”的特性,如果a等于false,那么b就没必要执行了。这就是为什么当command1成功,才会执行command2,当command2成功,才会执行command3。同样,
command1 || command2 || command3操作符||也有类似的特性。上面这个命令行,如果command1成功,那么command2和command3都不会被执行。如果command1失败,command2成功,那么command3就不会被执行。
这比起用if语句来判断失败,似乎更加巧妙和简洁,所以有人就借鉴了这种方式,在程序的代码里也使用这种方式。比如他们可能会写这样的代码:
  1. <span class="hljs-keyword">if</span> (action1() <span class="hljs-params">||</span> action2() && action3()) {
  2.   ...
  3. }
复制代码

你看得出来这代码是想干什么吗?action2和action3什么条件下执行,什么条件下不执行?也许稍微想一下,你知道它在干什么:“如果action1失败了,执行action2,如果action2成功了,执行action3”。然而那种语义,并不是直接的“映射”在这代码上面的。比如“失败”这个词,对应了代码里的哪一个字呢?你找不出来,因为它包含在了||的语义里面,你需要知道||的短路特性,以及逻辑或的语义才能知道这里面在说“如果action1失败……”。每一次看到这行代码,你都需要思考一下,这样积累起来的负荷,就会让人很累。

其实,这种写法是滥用了逻辑操作&&和||的短路特性。这两个操作符可能不执行右边的表达式,原因是为了机器的执行效率,而不是为了给人提供这种“巧妙”的用法。这两个操作符的本意,只是作为逻辑操作,它们并不是拿来给你代替if�语句的。也就是说,它们只是碰巧可以达到某些if语句的效果,但你不应该因此就用它来代替if语句。如果你这样做了,就会让代码晦涩难懂。
上面的代码写成笨一点的办法,就会清晰很多:
  1. <span class="hljs-keyword">if</span> (!action1()) {
  2.   <span class="hljs-keyword">if</span> (action2()) {
  3.     action3();
  4.   }
  5. }
复制代码

这里我很明显的看出这代码在说什么,想都不用想:如果action1()失败了,那么执行action2(),如果action2()成功了,执行action3()。你发现这里面的一一对应关系吗?if=如果,!=失败,…… 你不需要利用逻辑学知识,就知道它在说什么。
写无懈可击的代码在之前一节里,我提到了自己写的代码里面很少出现只有一个分支的if语句。我写出的if语句,大部分都有两个分支,所以我的代码很多看起来是这个样子:
  1. <span class="hljs-keyword">if</span> (...) {
  2.   <span class="hljs-keyword">if</span> (...) {
  3.     ...
  4.     <span class="hljs-built_in">return</span> <span class="hljs-literal">false</span>;
  5.   } <span class="hljs-keyword">else</span> {
  6.     <span class="hljs-built_in">return</span> <span class="hljs-literal">true</span>;
  7.   }
  8. } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (...) {
  9.   ...
  10.   <span class="hljs-built_in">return</span> <span class="hljs-literal">false</span>;
  11. } <span class="hljs-keyword">else</span> {
  12.   <span class="hljs-built_in">return</span> <span class="hljs-literal">true</span>;
  13. }
复制代码

使用这种方式,其实是为了无懈可击的处理所有可能出现的情况,避免漏掉corner case。每个if语句都有两个分支的理由是:如果if的条件成立,你做某件事情;但是如果if的条件不成立,你应该知道要做什么另外的事情。不管你的if有没有else,你终究是逃不掉,必须得思考这个问题的。
很多人写if语句喜欢省略else的分支,因为他们觉得有些else分支的代码重复了。比如我的代码里,两个else分支都是return true。为了避免重复,他们省略掉那两个else分支,只在最后使用一个return true。这样,缺了else分支的if语句,控制流自动“掉下去”,到达最后的return true。他们的代码看起来像这个样子:
  1. <span class="hljs-keyword">if</span> (...) {
  2.   <span class="hljs-keyword">if</span> (...) {
  3.     ...
  4.     <span class="hljs-built_in">return</span> <span class="hljs-literal">false</span>;
  5.   }
  6. } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (...) {
  7.   ...
  8.   <span class="hljs-built_in">return</span> <span class="hljs-literal">false</span>;
  9. }
  10. <span class="hljs-built_in">return</span> <span class="hljs-literal">true</span>;
复制代码

这种写法看似更加简洁,避免了重复,然而却很容易出现疏忽和漏洞。嵌套的if语句省略了一些else,依靠语句的“控制流”来处理else的情况,是很难正确的分析和推理的。如果你的if条件里使用了&&和||之类的逻辑运算,就更难看出是否涵盖了所有的情况。
由于疏忽而漏掉的分支,全都会自动“掉下去”,最后返回意想不到的结果。即使你看一遍之后确信是正确的,每次读这段代码,你都不能确信它照顾了所有的情况,又得重新推理一遍。这简洁的写法,带来的是反复的,沉重的头脑开销。这就是所谓“面条代码”,因为程序的逻辑分支,不是像一棵枝叶分明的树,而是像面条一样绕来绕去。
另外一种省略else分支的情况是这样:
  1. <span class="hljs-built_in">String</span> s = <span class="hljs-string">""</span>;
  2. <span class="hljs-keyword">if</span> (x < <span class="hljs-number">5</span>) {
  3.   s = <span class="hljs-string">"ok"</span>;
  4. }
复制代码

写这段代码的人,脑子里喜欢使用一种“缺省值”的做法。s缺省为null,如果x<5,那么把它改变(mutate)成“ok”。这种写法的缺点是,当x<5不成立的时候,你需要往上面看,才能知道s的值是什么。这还是你运气好的时候,因为s就在上面不远。很多人写这种代码的时候,s的初始值离判断语句有一定的距离,中间还有可能插入一些其它的逻辑和赋值操作。这样的代码,把变量改来改去的,看得人眼花,就容易出错。
现在比较一下我的写法:
  1. <span class="hljs-built_in">String</span> s;
  2. <span class="hljs-keyword">if</span> (x < <span class="hljs-number">5</span>) {
  3.   s = <span class="hljs-string">"ok"</span>;
  4. } <span class="hljs-keyword">else</span> {
  5.   s = <span class="hljs-string">""</span>;
  6. }
复制代码

这种写法貌似多打了一两个字,然而它却更加清晰。这是因为我们明确的指出了x<5不成立的时候,s的值是什么。它就摆在那里,它是""(空字符串)。注意,虽然我也使用了赋值操作,然而我并没有“改变”s的值。s一开始的时候没有值,被赋值之后就再也没有变过。我的这种写法,通常被叫做更加“函数式”,因为我只赋值一次。
如果我漏写了else分支,Java编译器是不会放过我的。它会抱怨:“在某个分支,s没有被初始化。”这就强迫我清清楚楚的设定各种条件下s的值,不漏掉任何一种情况。
当然,由于这个情况比较简单,你还可以把它写成这样:
  1. <span class="hljs-built_in">String</span> s = x < <span class="hljs-number">5</span> ? <span class="hljs-string">"ok"</span> : <span class="hljs-string">""</span>;
复制代码
对于更加复杂的情况,我建议还是写成if语句为好。







回复

使用道具 举报

0

主题

6

帖子

165

积分

注册会员

Rank: 2

积分
165
发表于 2019-1-25 02:52:41 | 显示全部楼层
不错
回复

使用道具 举报

0

主题

6

帖子

218

积分

中级会员

Rank: 3Rank: 3

积分
218
发表于 2019-1-25 07:26:11 | 显示全部楼层
支持一下
回复

使用道具 举报

0

主题

5

帖子

142

积分

注册会员

Rank: 2

积分
142
发表于 2019-2-7 14:57:42 | 显示全部楼层
内容引起舒适
回复

使用道具 举报

0

主题

7

帖子

195

积分

注册会员

Rank: 2

积分
195
发表于 2019-2-19 13:45:24 | 显示全部楼层
可以的,感谢楼主分享,给个赞。
回复

使用道具 举报

0

主题

5

帖子

127

积分

注册会员

Rank: 2

积分
127
发表于 2019-2-23 11:22:57 | 显示全部楼层
很不容易啊
回复

使用道具 举报

0

主题

6

帖子

188

积分

注册会员

Rank: 2

积分
188
发表于 2019-3-5 00:56:40 | 显示全部楼层
码字不易,回复一下以示尊敬
回复

使用道具 举报

0

主题

5

帖子

102

积分

注册会员

Rank: 2

积分
102
发表于 2019-3-13 01:30:33 | 显示全部楼层
听说评论有积分不知道真假
回复

使用道具 举报

0

主题

4

帖子

101

积分

注册会员

Rank: 2

积分
101
发表于 2019-3-14 21:31:22 | 显示全部楼层
暴雨哭泣终于找到了一篇能让我慢慢学的文章了,十分感谢博主大大!!!
回复

使用道具 举报

0

主题

4

帖子

106

积分

注册会员

Rank: 2

积分
106
发表于 2019-3-26 01:36:11 | 显示全部楼层
很好的分享
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|learun开发社区 - 力软.net/java快速开发平台官方论坛 ( 沪ICP备14034717号 )

GMT+8, 2021-10-19 23:55 , Processed in 0.209688 second(s), 25 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表