必赢亚洲手机app下载


花色搭建

三尾鱼的微笑必赢亚洲手机app

编程的灵性

编程是一种创立性的行事,是一门艺术。精通任何一门艺术,都急需过多的演习和精通,所以那边提出的“智慧”,并不是称呼一天瘦十斤的减肥药,它并无法代表你自己的费劲。可是由于软件行业喜欢标新立异,喜欢把大约的事体搞复杂,我期待那么些文字能给迷惑中的人们指出部分没错的倾向,让他俩少走一些弯路,基本形成一分耕耘一分收获。

反复推敲代码

既是“天才是百分之一的灵感,百分之九十九的汗珠”,那自己先来啄磨那汗水的局地吗。有人问我,进步编程水平最有效的措施是哪些?我想了很久,终于意识最管用的不二法门,其实是反反复复地修改和切磋代码。

在IU的时候,由于Dan
Friedman的严苛教育,我们以写出冗长复杂的代码为耻。假使你代码�多写了几行,那老顽童就会哈哈大笑,说:“当年自己解决这么些题材,只写了5行代码,你回去再思索呢……”
当然,有时候他只是夸张一下,故意激起你的,其实没有人能只用5行代码落成。但是那种提炼代码,减弱冗余的习惯,却由此深切了我的骨髓。

稍微人喜好炫耀自己写了有点有点万行的代码,就好像代码的多寡是衡量编程水平的规范。然则,假如您总是匆匆写出代码,却没有回头去推敲,修改和提纯,其实是不可以升高编程水平的。你会创设出更为多平庸甚至不好的代码。在这种含义上,很几人所谓的“工作经历”,跟她代码的成色,其实不肯定成正比。若是有几十年的干活经验,却并未回头去提炼和反省自己的代码,那么他恐怕还不如一个唯有一两年经历,却喜欢反复推敲,仔细领悟的人。

有位作家说得好:“看一个大作家的水准,不是看他公布了不怎么文字,而要看他的废纸篓里扔掉了稍稍。”
我觉着无异的辩解适用于编程。好的程序员,他们删掉的代码,比留下来的还要多广大。假诺您瞧瞧一个人写了不少代码,却不曾删掉多少,那她的代码一定有许多杂质。

就如艺术学文章一样,代码是不容许轻易的。灵感就像总是零零星星,陆陆续续到来的。任什么人都不容许一笔呵成,固然再决定的程序员,也须要经过一段时间,才能觉察最简便易行优雅的写法。有时候你频仍提炼一段代码,觉得到了顶峰,无法再革新了,不过过了几个月再回头来看,又发现众多得以改进和简化的地方。那跟写小说一模一样,回头看多少个月照旧几年前写的东西,你总能发现部分革新。

之所以一旦频仍提炼代码已经不复有拓展,那么您可以临时把它放下。过多少个礼拜依然多少个月再回头来看,也许就有万物更新的灵感。那样反反复复很数十次自此,你就累积起了灵感和聪明,从而可以在碰着新题材的时候平昔朝正确,或者接近正确的样子发展。

写优雅的代码

大千世界都讨厌“面条代码”(spaghetti
code),因为它就像是面条一样绕来绕去,无法理清头绪。那么优雅的代码一般是如何形态的啊?经过长年累月的考察,我发觉优雅的代码,在形象上有一些明明的特色。

假若大家忽视具体的内容,从大体上结构上来看,优雅的代码看起来似乎一些井井有条,套在一齐的盒子。即使跟整理房间做一个类比,就很不难领悟。如果你把拥有物品都丢在一个很大的抽屉里,那么它们就会全都混在联名。你就很难整理,很难急忙的找到须求的东西。不过即使你在抽屉里再放多少个小盒子,把物品分门别类放进去,那么它们就不会各处乱跑,你就足以相比便于的找到和管制它们。

淡雅的代码的另一个风味是,它的逻辑大体上看起来,是枝丫分明的树状结构(tree)。那是因为程序所做的大致全体工作,都是音信的传递和支行。你可以把代码看成是一个电路,电流经过导线,分流或者合并。假诺您是这般考虑的,你的代码里就会相比少出现唯有一个拨出的if语句,它看起来就会像那几个样子:

if (...) {
  if (...) {
    ...
  } else {
    ...
  }
} else if (...) {
  ...
} else {
  ...
}

只顾到了啊?在我的代码里面,if语句几乎连接有四个分支。它们有可能嵌套,有多层的缩进,而且else分支里面有可能出现少量再次的代码。然则那样的布局,逻辑却相当严密和清晰。在后头我会告诉你为啥if语句最好有七个支行。

写模块化的代码

多少人吵着闹着要让程序“模块化”,结果他们的做法是把代码分部到几个文本和目录里面,然后把这一个目录或者文件叫做“module”。他们竟然把那几个目录分放在差其余VCS
repo里面。结果那样的作法并从未带来合营的通畅,而是带来了广大的难为。那是因为她们实际并不知道什么叫做“模块”,肤浅的把代码切割开来,分放在不相同的职位,其实不单无法达到模块化的目标,而且制作了不要求的难为。

当真的模块化,并不是文本意义上的,而是逻辑意义上的。一个模块应该像一个电路芯片,它有定义出色的输入和出口。实际上一种很好的模块化方法早已经存在,它的名字称为“函数”。每一个函数都有鲜明的输入(参数)和出口(再次来到值),同一个文本里可以包含五个函数,所以您实在根本不需求把代码分开在多少个公文或者目录里面,同样能够成功代码的模块化。我得以把代码全都写在同一个文书里,却照样是非常模块化的代码。

想要达到很好的模块化,你须要完结以下几点:

  • 避免写太长的函数。如果发现函数太大了,就应当把它拆分成多少个更小的。日常自己写的函数长度都不当先40行。相比一下,一般台式机电脑显示屏所能容纳的代码行数是50行。我能够洞察的看见一个40行的函数,而不须要滚屏。只有40行而不是50行的原故是,我的眼珠子不转的话,最大的眼光只看收获40行代码。

    比方自身看代码不转眼球的话,我就能把整片代码完整的映射到自身的视觉神经里,那样就是突然闭上眼睛,我也能看得见这段代码。我发现闭上眼睛的时候,大脑可以尤其可行地处理代码,你能想象那段代码可以成为何其余的模样。40行并不是一个很大的范围,因为函数里面相比较复杂的部分,往往已经被我领到出来,做成了更小的函数,然后从原本的函数里面调用。

  • 打造小的工具函数。若是您仔细观看代码,就会发觉其实里面有很多的双重。这个常用的代码,不管它有多短,提取出来做成函数,都可能是会有实益的。有些拉扯函数也许就唯有两行,可是它们却能大大简化主要函数里面的逻辑。

    有些人不爱好使用小的函数,因为他俩想幸免函数调用的支出,结果他们写出几百行之大的函数。那是一种过时的传统。现代的编译器都能活动的把小的函数内联(inline)到调用它的地点,所以根本不发出函数调用,也就不会暴发其他多余的支出。

    无异于的片段人,也爱使用宏(macro)来代替小函数,那也是一种过时的观念。在早期的C语言编译器里,只有宏是静态“内联”的,所以他们使用宏,其实是为着达到内联的目的。不过能或不能内联,其实并不是宏与函数的有史以来分裂。宏与函数有着光辉的界别(这一个自己之后再讲),应该尽量防止使用宏。为了内联而使用宏,其实是滥用了宏,那会挑起各类种种的分神,比如使程序难以领会,难以调试,简单出错等等。

  • 各种函数只做一件简单的事情。有些人喜爱制作一些“通用”的函数,既可以做这几个又可以做更加,它的内部依照某些变量和标准,来“选用”那几个函数所要做的作业。比如,你或许写出如此的函数:

    void foo() {
      if (getOS().equals("MacOS")) {
        a();
      } else {
        b();
      }
      c();
      if (getOS().equals("MacOS")) {
        d();
      } else {
        e();
      }
    }
    

    写那几个函数的人,根据系统是还是不是为“MacOS”来做不一致的工作。你可以见到这一个函数里,其实只有c()是三种系统共有的,而任何的a(),
    b(), d(), e()都属于区其他支行。

    那种“复用”其实是风险的。即使一个函数可能做三种工作,它们中间共同点少于它们的分裂点,那您最好就写五个分裂的函数,否则那几个函数的逻辑就不会很清楚,简单并发谬误。其实,下边这么些函数可以改写成多个函数:

    void fooMacOS() {
      a();
      c();
      d();
    }
    

    void fooOther() {
      b();
      c();
      e();
    }
    

    只要您发现两件业务半数以上情节同样,唯有少数差异,多半时候你可以把相同的一部分提取出来,做成一个赞助函数。比如,要是你有个函数是那般:

    void foo() {
      a();
      b()
      c();
      if (getOS().equals("MacOS")) {
        d();
      } else {
        e();
      }
    }
    

    其中a()b()c()都是同样的,只有d()e()据悉系统有所差异。那么你可以把a()b()c()领到出来:

    void preFoo() {
      a();
      b()
      c();
    

    接下来成立三个函数:

    void fooMacOS() {
      preFoo();
      d();
    }
    

    void fooOther() {
      preFoo();
      e();
    }
    

    那样一来,大家既共享了代码,又成功了种种函数只做一件简单的作业。那样的代码,逻辑就愈加清晰。

  • 避免使用全局变量和类成员(class
    member)来传递消息,尽量使用部分变量和参数。有些人写代码,平常用类成员来传递音讯,似乎那样:

    class A {
      String x;
    
      void findX() {
         ...
         x = ...;
      }
    
      void foo() {
        findX();
        ...
        print(x);
      }
    }
    

    首先,他使用findX(),把一个值写入成员x。然后,使用x的值。这样,x就变成了findXprint以内的数据通道。由于x属于class A,那样程序就失去了模块化的协会。由于那三个函数信赖于成员x,它们不再有明显的输入和输出,而是依靠全局的多少。findXfoo不再可以离开class A而留存,而且由于类成员还有可能被其他代码改变,代码变得难以知晓,难以保障正确。

    如果您使用一些变量而不是类成员来传递音信,那么那八个函数就不必要借助于某一个class,而且越来越简单通晓,不易出错:

    String findX() {
       ...
       x = ...;
       return x;
    }
    void foo() {
      String x = findX();
      print(x);
    }
    

写可读的代码

些微人觉着写过多评释就足以让代码更加可读,但是却发现大失所望。注释不但没能让代码变得可读,反而由于大气的申明充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有不少的注释变得过时,要求立异。修改注释是卓殊大的负责,所以大气的申明,反而成为了妨碍创新代码的阻力。

实在,真正优雅可读的代码,是大概不要求注释的。如若你意识要求写过多声明,那么您的代码肯定是含混晦涩,逻辑不清楚的。其实,程序语言相比自然语言,是尤为强劲而谨慎的,它其实所有自然语言最紧要的要素:主语,谓语,宾语,名词,动词,倘若,那么,否则,是,不是,……
所以倘使您充足利用了程序语言的表明能力,你完全可以用程序本身来发布它到底在干什么,而不要求自然语言的扶持。

有个其余时候,你也许会为了绕过其余部分代码的宏图问题,采纳局地违反直觉的作法。那时候你能够动用很短注释,表达为什么要写成那奇怪的旗帜。那样的情景应当少出现,否则那意味任何代码的规划都有难点。

一旦没能合理使用程序语言提供的优势,你会发现先后仍旧很难懂,以至于必要写注释。所以自己现在告诉你有些要义,也许可以协理您大大缩小写注释的必不可少:

  1. 动用有意义的函数和变量名字。如若您的函数和变量的名字,可以切实的叙述它们的逻辑,那么您就不必要写注释来表达它在干什么。比如:

    // put elephant1 into fridge2
    put(elephant1, fridge2);
    

    由于自家的函数名put,加上三个有含义的变量名elephant1fridge2,已经证实了那是在干什么(把大象放进冰橱),所以地方那句注释完全没有要求。

  2. 局地变量应该尽可能接近使用它的地点。有些人喜欢在函数最早先定义很多有的变量,然后在底下很远的地点使用它,如同这几个样子:

    void foo() {
      int index = ...;
      ...
      ...
      bar(index);
      ...
    }
    

    鉴于那当中都未曾行使过index,也从不更改过它所依赖的数量,所以那么些变量定义,其实能够挪到接近使用它的地方:

    void foo() {
      ...
      ...
      int index = ...;
      bar(index);
      ...
    }
    

    这么读者看到bar(index),不须要向上看很远就能觉察index是什么算出来的。而且那种短距离,可以升高读者对此那里的“计算顺序”的了解。否则一旦index在顶上,读者可能会存疑,它事实上保存了某种会转变的数目,或者它后来又被改动过。假使index放在下边,读者就驾驭的领会,index并不是保留了怎么着可变的值,而且它算出来将来就没变过。

    借使你看透了部分变量的精神——它们就是电路里的导线,那你就能更好的精通中远距离的益处。变量定义离用的地点越近,导线的长短就越短。你不需求摸着一根导线,绕来绕去找很远,就能发现收到它的端口,那样的电路就更便于领会。

  3. 有的变量名字应该简短。这一般跟第一点相冲突,简短的变量名怎么可能有意义吗?注意自身那边说的是有些变量,因为它们处于局地,再添加第2点已经把它内置离使用地点尽量近的地点,所以根据上下文你就会简单明白它的意思:

    譬如,你有一个有的变量,表示一个操作是不是中标:

    boolean successInDeleteFile = deleteFile("foo.txt");
    if (successInDeleteFile) {
      ...
    } else {
      ...
    }
    

    本条片段变量successInDeleteFile大可不必这么啰嗦。因为它只用过四遍,而且用它的地方就在底下一行,所以读者可以轻松发现它是deleteFile回来的结果。如若您把它改名为success,其实读者依据一些上下文,也明白它意味着”success
    in deleteFile”。所以你可以把它改成那样:

    boolean success = deleteFile("foo.txt");
    if (success) {
      ...
    } else {
      ...
    }
    

    如此那般的写法不但没漏掉任何有效的语义音讯,而且越来越易读。successInDeleteFile这种“camelCase”,若是跨越了多少个单词连在一起,其实是很刺眼的东西。所以假诺您能用一个单词表示一致的意思,这自然更好。

  4. 不要重用局地变量。很多个人写代码不希罕定义新的一些变量,而喜欢“重用”同一个片段变量,通过反复对它们进行赋值,来代表完全分歧意思。比如那样写:

    String msg;
    if (...) {
      msg = "succeed";
      log.info(msg);
    } else {
      msg = "failed";
      log.info(msg);
    }
    

    虽说那样在逻辑上是平素不难点的,可是却不易通晓,不难混淆视听。变量msg四回被赋值,表示完全差距的四个值。它们立时被log.info动用,没有传递到其它地方去。那种赋值的做法,把一些变量的效率域不需求的增大,令人以为�它恐怕在明日变动,也许会在其余地方被选用。更好的做法,其实是概念七个变量:

    if (...) {
      String msg = "succeed";
      log.info(msg);
    } else {
      String msg = "failed";
      log.info(msg);
    }
    

    鉴于那多个msg变量的成效域仅限于它们所处的if语句分支,你可以很清楚的看来这七个msg被应用的限量,而且知道它们之间从未其余关系。

  5. 把纷纭的逻辑提取出来,做成“协理函数”。有些人写的函数很长,以至于看不清楚里面的说话在干什么,所以他们误以为需求写注释。假如你细心考察这几个代码,就会意识不明晰的那片代码,往往可以被提取出来,做成一个函数,然后在原先的地方调用。由于函数有一个名字,那样您就足以应用有含义的函数名来代表注释。举一个例子:

    ...
    // put elephant1 into fridge2
    openDoor(fridge2);
    if (�elephant1.�alive()) {
      ...
    } else {
       ...
    }
    closeDoor(fridge2);
    ...
    

    设若您把那片代码提议去定义成一个函数:

    void put(Elephant elephant, Fridge fridge) {
      openDoor(fridge);
      if (�elephant.alive()) {
        ...
      } else {
         ...
      }
      closeDoor(fridge);
    }
    

    诸如此类原本的代码就可以改成:

    ...
    put(elephant1, fridge2);
    ...
    

    更进一步显然,而且注释也没要求了。

  6. 把复杂的表达式提取出来,做成中间变量。有些人听说“函数式编程”是个好东西,也不通晓它的着实含义,就在代码里大批量运用嵌套的函数。像那样:

    Pizza pizza = makePizza(crust(salt(), butter()),
       topping(onion(), tomato(), sausage()));
    

    那样的代码一行太长,而且嵌套太多,不便于看了解。其实操练有素的函数式程序员,都知晓中间变量的补益,不会盲目标利用嵌套的函数。他们会把那代码变成那样:

    Crust crust = crust(salt(), butter());
    Topping topping = topping(onion(), tomato(), sausage());
    Pizza pizza = makePizza(crust, topping);
    

    如此那般写,不但使得地决定了单行代码的尺寸,而且由于引入的高中级变量具有“意义”,步骤清晰,变得很简单了然。

  7. 在合理的地点换行。对于绝大多数的程序语言,代码的逻辑是和空白字符无关的,所以你可以在大致任何地方换行,你也可以不换行。那样的言语设计是个好东西,因为它给了程序员自由支配自己代码格式的力量。可是,它也引起了部分题材,因为众多少人不通晓哪些客观的换行。

有点人喜爱使用IDE的自行换行机制,编辑之后用一个热键把全副代码重新格式化两次,IDE就会把超过行宽限制的代码自动折行。但是那种活动那行,往往没有基于代码的逻辑来进展,不可能协理领悟代码。自动换行之后可能暴发这么的代码:

if (someLongCondition1() && someLongCondition2() && someLongCondition3() && 
  someLongCondition4()) {
  ...
}

由于someLongCondition4()超过了行宽限制,被编辑器自动换来了下面一行。即使满意了行宽限制,换行的岗位却是卓殊自由的,它并不可以辅助人精通那代码的逻辑。那多少个boolean表明式,全都用&&连日,所以它们其实处于相同的地位。为了发挥那或多或少,当必要折行的时候,你应当把每一个表明式都放到新的一行,如同这几个样子:

if (someLongCondition1() && 
    someLongCondition2() && 
    someLongCondition3() && 
    someLongCondition4()) {
  ...
}

如此那般每一个规范都对齐,里面的逻辑就很清楚了。再举个例证:

log.info("failed to find file {} for command {}, with exception {}", file, command,
  exception);

那行因为太长,被活动折行成那一个样子。filecommandexception本来是如出一辙类东西,却有多少个留在了第一行,最终一个被折到第二行。它就不如手动换行成这些样子:

log.info("failed to find file {} for command {}, with exception {}",
  file, command, exception);

把格式字符串单独放在一行,而把它的参数一并雄居其余一行,那样逻辑就更加显然。

为了幸免IDE把这个手动调整好的换行弄乱,很多IDE(比如AMDliJ)的自行格式化设定里都有“保留原来的换行符”的设定。如若你意识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++),你完全可以把它拆成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)必赢亚洲手机app,,……
    没有人应该清楚,或者去追究那几个是怎样看头。

  • 永久不要简单花括号。很多言语允许你在某种情况下省略掉花括号,比如C,Java都同意你在if语句里面只有一句话的时候省略掉花括号:

    if (...) 
      action1();
    

    咋一看少打了八个字,多好。可是那其实常常引起意外的标题。比如,你后来想要加一句话action2()到这么些if里面,于是你就把代码改成:

    if (...) 
      action1();
      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,你就应当考虑改写那些轮回。改写循环的格局有三种:

    1. 只要出现了continue,你频仍只须求把continue的口径反向,就足以排除continue。
    2. 设若出现了break,你频还能够把break的规则,合并到循环尾部的终止条件里,从而去掉break。
    3. 有时候你可以把break替换成return,从而去掉break。
    4. 万一以上都失利了,你恐怕可以把循环之中复杂的片段提取出来,做成函数调用,之后continue或者break就足以去掉了。

    下边我对那一个状况举一些事例。

    动静1:下边那段代码里面有一个continue:

    List<String> goodNames = new ArrayList<>();
    for (String name: names) {
      if (name.contains("bad")) {
        continue;
      }
      goodNames.add(name);
      ...
    }  
    

    它说:“倘诺name含有’bad’这几个词,跳过后边的循环代码……”
    注意,那是一种“负面”的叙说,它不是在告知你几时“做”一件事,而是在报告您如曾几何时候“不做”一件事。为了明白它究竟在干什么,你无法不搞清楚continue会导致什么样语句被跳过了,然后脑子里把逻辑反个向,你才能了解它到底想做什么样。那就是为什么含有continue和break的循环不不难领悟,它们凭借“控制流”来描述“不做哪些”,“跳过如何”,结果到最终你也没搞精晓它到底“要做怎么样”。

    实际上,我们只必要把continue的原则反向,那段代码就足以很简单的被转换成等价的,不含continue的代码:

    List<String> goodNames = new ArrayList<>();
    for (String name: names) {
      if (!name.contains("bad")) {
        goodNames.add(name);
        ...
      }
    }  
    

    goodNames.add(name);和它今后的代码全体被放到了if里面,多了一层缩进,然则continue却未曾了。你再读那段代码,就会发现越来越明显。因为它是一种尤其“正面”地叙述。它说:“在name不包罗’bad’那么些词的时候,把它加到goodNames的链表里面……”

    场地2:for和while底部都有一个循环往复的“终止条件”,那本来应该是以此循环唯一的淡出标准。若是您在循环当中有break,它实质上给那个轮回增添了一个退出标准。你往往只需求把那个规则合并到循环底部,就可以去掉break。

    诸如上面那段代码:

    while (condition1) {
      ...
      if (condition2) {
        break;
      }
    }
    

    当condition创建的时候,break会退出循环。其实您只要求把condition2反转之后,放到while尾部的告一段落条件,就可以去掉那种break语句。改写后的代码如下:

    while (condition1 && !condition2) {
      ...
    }
    

    那种境况表面上相似只适用于break出现在循环起来或者末尾的时候,但是实际上多数时候,break都得以经过某种方式,移动到循环的起始或者末尾。具体的事例我临前卫未,等并发的时候再加进去。

    气象3:很多break退出循环之后,其实接下去就是一个return。那种break往往可以一贯换成return。比如下边那几个例子:

    public boolean hasBadName(List<String> names) {
        boolean result = false;
    
        for (String name: names) {
            if (name.contains("bad")) {
                result = true;
                break;
            }
        }
        return result;
    }
    

    其一函数检查names链表里是还是不是留存一个名字,包罗“bad”这一个词。它的大循环里含有一个break语句。那一个函数可以被改写成:

    public boolean hasBadName(List<String> names) {
        for (String name: names) {
            if (name.contains("bad")) {
                return true;
            }
        }
        return false;
    }
    

    考订后的代码,在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语句来判定战败,就如更为巧妙和简单,所以有人就借鉴了那种措施,在先后的代码里也利用那种艺术。比如他们或者会写这么的代码:

if (action1() || action2() && action3()) {
  ...
}

您看得出来这代码是想干什么吗?action2和action3怎么标准下执行,什么条件下不履行?也许有些想转手,你了解它在干什么:“假设action1败北了,执行action2,假若action2成功了,执行action3”。然则那种语义,并不是直接的“映射”在那代码上边的。比如“失败”那几个词,对应了代码里的哪一个字呢?你找不出去,因为它包罗在了||的语义里面,你需求精通||的阻隔特性,以及逻辑或的语义才能驾驭那之中在说“如果action1战败……”。每便探望那行代码,你都须要思考一下,那样积累起来的负载,就会令人很累。

其实,那种写法是滥用了逻辑操作&&||的围堵特性。那四个操作符可能不实施右侧的表达式,原因是为着机器的实施功能,而不是为了给人提供那种“巧妙”的用法。那四个操作符的原意,只是当作逻辑操作,它们并不是拿来给您代替if�语句的。也就是说,它们只是恰好可以达标某些if语句的效益,但您不应有为此就用它来顶替if语句。倘诺你那样做了,就会让代码晦涩难懂。

上面的代码写成笨一点的章程,就会清楚很多:

if (!action1()) {
  if (action2()) {
    action3();
  }
}

此地我很显眼的看到那代码在说怎么,想都不用想:倘使action1()失利了,那么执行action2(),要是action2()成功了,执行action3()。你发觉那其中的相继对应提到啊?if=如果,!=败北,……
你不须要动用逻辑学知识,就知晓它在说什么样。

写无懈可击的代码

在前头一节里,我关系了温馨写的代码里面很少出现唯有一个分段的if语句。我写出的if语句,大多数都有八个支行,所以我的代码很多看起来是以此样子:

if (...) {
  if (...) {
    ...
    return false;
  } else {
    return true;
  }
} else if (...) {
  ...
  return false;
} else {
  return true;
}

行使那种方法,其实是为着无懈可击的拍卖所有可能出现的场地,防止漏掉corner
case。每个if语句都有三个分支的理由是:借使if的原则建立,你做某件事情;可是只要if的尺码不成立,你应该知道要做什么样其它的政工。不管你的if有没有else,你毕竟是逃不掉,必须得考虑那些标题标。

有的是人写if语句喜欢省略else的分层,因为她俩认为有点else分支的代码重复了。比如自己的代码里,四个else分支都是return true。为了防止再次,他们省略掉这七个else分支,只在最终采纳一个return true。这样,缺了else分支的if语句,控制流自动“掉下去”,到达最后的return true。他们的代码看起来像那几个样子:

if (...) {
  if (...) {
    ...
    return false;
  } 
} else if (...) {
  ...
  return false;
} 
return true;

那种写法看似尤其从简,防止了重新,然则却很简单并发疏忽和漏洞。嵌套的if语句不难了一部分else,依靠语句的“控制流”来处理else的情状,是很难正确的辨析和演绎的。倘使您的if条件里拔取了&&||等等的逻辑运算,就更难看出是还是不是含有了独具的状态。

是因为疏忽而漏掉的支行,全都会机动“掉下去”,最后回到意想不到的结果。尽管你看一次之后确信是合情合理的,每一次读那段代码,你都无法确信它照顾了装有的意况,又得重复演绎一次。那简单的写法,带来的是频仍的,沉重的心血花费。那就是所谓“面条代码”,因为程序的逻辑分支,不是像一棵枝叶显著的树,而是像面条一样绕来绕去。

除此以外一种省略else分支的景色是那般:

String s = "";
if (x < 5) {
  s = "ok";
}

写那段代码的人,脑子里喜欢使用一种“缺省值”的做法。s缺省为null,如果x<5,那么把它改变(mutate)成“ok”。那种写法的症结是,当x<5不树立的时候,你须要往下面看,才能知道s的值是什么。那依旧您运气好的时候,因为s就在上面不远。很多人写那种代码的时候,s的开始值离判断语句有早晚的相距,中间还有可能插入一些别样的逻辑和赋值操作。这样的代码,把变量改来改去的,看得人眼花,就便于出错。

现在相比一下我的写法:

String s;
if (x < 5) {
  s = "ok";
} else {
  s = "";
}

那种写法貌似多打了一两个字,但是它却更为清楚。这是因为大家明确的提议了x<5不树立的时候,s的值是什么。它就摆在那里,它是""(空字符串)。注意,尽管我也使用了赋值操作,可是我并不曾“改变”s的值。s一上马的时候从不值,被赋值之后就再也从不变过。我的那种写法,常常被称作尤其“函数式”,因为我只赋值四遍。

万一我漏写了else分支,Java编译器是不会放过自家的。它会埋怨:“在某个分支,s没有被初步化。”那就迫使我鲜明的设定各类条件下s的值,不疏漏任何一种情景。

自然,由于那么些处境比较不难,你还足以把它写成这么:

String s = x < 5 ? "ok" : "";

对此更为扑朔迷离的事态,我指出仍然写成if语句为好。

正确处理错误

动用有多个支行的if语句,只是我的代码可以直达无懈可击的中间一个缘故。那样写if语句的笔触,其实包涵了使代码可依赖的一种通用思想:穷举所有的处境,不疏漏任何一个。

�程序的多边效益,是拓展新闻处理。从一堆纷纭复杂,举棋不定的音讯中,排除掉绝半数以上“苦恼新闻”,找到自己必要的那么些。正确地对具有的“可能性”举办推导,就是写出无懈可击代码的主题理想。这一节自我来讲一讲,怎么着把那种思维用在错误处理上。

错误处理是一个古老的题材,然而经过了几十年,仍旧广大人没搞通晓。Unix的体系API手册,一般都会报告你也许出现的再次来到值和错误消息。比如,Linux的read系统调用手册里面有如下内容:

<pre>
RETURN VALUE
On success, the number of bytes read is returned…

On error, -1 is returned, and errno is set appropriately.
<p>
ERRORS

EAGAIN, EBADF, EFAULT, EINTR, EINVAL, …
</pre>

众多初学者,都会忘记检查read的重临值是不是为-1,觉得每一次调用read都得检查再次来到值真繁琐,不检查貌似也相安无事。那种想法其实是很惊险的。如若函数的重返值告诉您,要么重临一个正数,表示读到的数量长度,要么回到-1,那么您就亟须求对这些-1作出相应的,有意义的处理。千万不要认为你可以忽略那些奇异的再次来到值,因为它是一种“可能性”。代码漏掉任何一种可能出现的状态,都可能暴发意料之外的灾害结果。

对此Java来说,那相对有利一些。Java的函数假诺现身难点,一般经过丰富(exception)来表示。你可以把卓殊加上函数本来的重回值,看成是一个“union类型”。比如:

String foo() throws MyException {
  ...
}

那边MyException是一个错误再次回到。你可以认为这几个函数重返一个union类型:{String, MyException}。任何调用foo的代码,必须对MyException作出合理的拍卖,才有可能保障程序的没错运行。Union类型是一种格外先进的花色,近年来唯有极少数言语(比如Typed
Racket)具有那连串型,我在此间涉及它,只是为着便于解释概念。明白了概念之后,你实在可以在头脑里完成一个union类型系统,那样使用普通的言语也能写出可依赖的代码。

是因为Java的体系系统强制必要函数在类型里面注解可能出现的不得了,而且强制调用者处理或者出现的相当,所以基本上不容许出现是因为疏忽而漏掉的情况。但有点Java程序员有一种恶习,使得那种安全机制几乎完全失效。每当编译器报错,说“你未曾catch这么些foo函数可能出现的出色”时,有些人想都不想,直接把代码改成这么:

try {
  foo();
} catch (Exception e) {}

要么最多在其间放个log,或者干脆把自己的函数类型上丰裕throws Exception,那样编译器就不再抱怨。这个做法貌似很省心,可是都是谬误的,你终究会为此付出代价。

即使您把更加catch了,忽略掉,那么你就不知情foo其实失败了。那如同开车时观察街头写着“前方施工,道路关闭”,还一连往前开。那自然迟早会出难点,因为您根本不精晓自己在干什么。

catch非常的时候,你不应该使用Exception这么大规模的花色。你应有正好catch可能暴发的那种极度A。使用大规模的不胜类型有很大的标题,因为它会不留心的catch住此外的丰硕(比如B)。你的代码逻辑是基于判断A是或不是现身,可你却catch所有的老大(Exception类),所以当其他的那些B出现的时候,你的代码就会并发莫明其妙的难题,因为您以为A出现了,而实际上它从不。这种bug,有时候甚至动用debugger都不便察觉。

要是你在投机函数的品类丰富throws Exception,那么您就不可幸免的内需在调用它的地点处理这些那个,借使调用它的函数也写着throws Exception,那毛病就传得更远。我的阅历是,尽量在丰盛出现的及时就作出处理。否则若是你把它回到给您的调用者,它可能根本不晓得该怎么办了。

此外,try { … }
catch里面,应该包罗尽量少的代码。比如,若是foobar都可能发生万分A,你的代码应该尽量写成:

try {
  foo();
} catch (A e) {...}

try {
  bar();
} catch (A e) {...}

而不是

try {
  foo();
  bar();
} catch (A e) {...}

首先种写法能肯定的鉴别是哪一个函数出了难点,而第两种写法全都混在一块。明确的辨识是哪一个函数出了难题,有不可胜言的益处。比如,借使你的catch代码里面富含log,它可以提需要您越是规范的错误音信,那样会大大地加快你的调节进程。

正确处理null指针

穷举的思考是那样的有用,根据那些规律,大家得以推出一些主导规则,它们得以让你无懈可击的拍卖null指针。

首先你应当清楚,许多言语(C,C++,Java,C#,……)的花色系统对于null的拍卖,其实是全然错误的。这几个错误源自于Tony
Hoare
最早的宏图,Hoare把那么些指鹿为马称为自己的“billion
dollar
mistake
”,因为出于它所发出的财产和人工损失,远远超越十亿美元。

那一个语言的品类系统允许null出现在其余对象(指针)类型可以出现的地点,可是null其实根本不是一个法定的目的。它不是一个String,不是一个Integer,也不是一个自定义的类。null的品种本来应该是NULL,也就是null自己。根据这几个宗旨看法,大家推导出以下条件:

  • 尽心尽力不要发生null指针。尽量不要用null来初始化变量,函数尽量不要回来null。若是你的函数要回去“没有”,“出错了”之类的结果,尽量选用Java的更加机制。就算写法上稍微别扭,可是Java的非常,和函数的重返值合并在一块儿,基本上可以算作union类型来用。比如,如果你有一个函数find,可以帮您找到一个String,也有可能什么也找不到,你可以这么写:

    public String find() throws NotFoundException {
      if (...) {
        return ...;
      } else {
        throw new NotFoundException();
      }
    

}

  Java的类型系统会强制你catch这个NotFoundException,所以你不可能像漏掉检查null一样,漏掉这种情况。Java的异常也是一个比较容易滥用的东西,不过我已经在上一节告诉你如何正确的使用异常。

  Java的try...catch语法相当的繁琐和蹩脚,所以如果你足够小心的话,像`find`这类函数,也可以返回null来表示“没找到”。这样稍微好看一些,因为你调用的时候不必用try...catch。很多人写的函数,返回null来表示“出错了”,这其实是对null的误用。“出错了”和“没有”,其实完全是两码事。“没有”是一种很常见,正常的情况,比如查哈希表没找到,很正常。“出错了”则表示罕见的情况,本来正常情况下都应该存在有意义的值,偶然出了问题。如果你的函数要表示“出错了”,应该使用异常,而不是null。

- 不要catch NullPointerException。有些人写代码很nice,他们喜欢“容错”。首先他们写一些函数,这些函数里面不大小心,没检查null指针:

void foo() {
String found = find();
int len = found.length();

}

当foo调用产生了异常,他们不管三七二十一,就把调用的地方改成这样:

try {
foo();
} catch (Exception e) {

}

这样当found是null的时候,NullPointerException就会被捕获并且得到处理。这其实是很错误的作法。首先,上一节已经提到了,`catch (Exception e)`这种写法是要绝对避免的,因为它捕获所有的异常,包括NullPointerException。这会让你意外地捕获try语句里面出现的NullPointerException,从而把代码的逻辑搅得一塌糊涂。

另外就算你写成`catch (NullPointerException e)`也是不可以的。由于foo的内部缺少了null检查,才出现了NullPointerException。现在你不对症下药,倒把每个调用它的地方加上catch,以后你的生活就会越来越苦。正确的做法应该是改动foo,而不改调用它的代码。foo应该被改成这样:

void foo() {
String found = find();
if (found != null) {
int len = found.length();

} else {

}
}

 在null可能出现的当时就检查它是否是null,然后进行相应的处理。

- 不要把null放进“容器数据结构”里面。所谓容器(collection),是指一些对象以某种方式集合在一起,所以null不应该被放进Array,List,Set等结构,不应该出现在Map的key或者value里面。把null放进容器里面,是一些莫名其妙错误的来源。因为对象在容器里的位置一般是动态决定的,所以一旦null从某个入口跑进去了,你就很难再搞明白它去了哪里,你就得被迫在所有从这个容器里取值的位置检查null。你也很难知道到底是谁把它放进去的,代码多了就导致调试极其困难。

解决方案是:如果你真要表示“没有”,那你就干脆不要把它放进去(Array,List,Set没有元素,Map根本没那个entry),或者你可以指定一个特殊的,真正合法的对象,用来表示“没有”。

需要指出的是,类对象并不属于容器。所以null在必要的时候,可以作为对象成员的值,表示它不存在。比如:

class A {
String name = null;

}

之所以可以这样,是因为null只可能在A对象的name成员里出现,你不用怀疑其它的成员因此成为null。所以你每次访问name成员时,检查它是否是null就可以了,不需要对其他成员也做同样的检查。

- 函数调用者:明确理解null所表示的意义,尽早检查和处理null返回值,减少它的传播。null很讨厌的一个地方,在于它在不同的地方可能表示不同的意义。有时候它表示“没有”,“没找到”。有时候它表示“出错了”,“失败了”。有时候它甚至可以表示“成功了”,…… 这其中有很多误用之处,不过无论如何,你必须理解每一个null的意义,不能给混淆起来。

如果你调用的函数有可能返回null,那么你应该在第一时间对null做出“有意义”的处理。比如,上述的函数`find`,返回null表示“没找到”,那么调用`find`的代码就应该在它返回的第一时间,检查返回值是否是null,并且对“没找到”这种情况,作出有意义的处理。

“有意义”是什么意思呢?我的意思是,使用这函数的人,应该明确的知道在拿到null的情况下该怎么做,承担起责任来。他不应该只是“向上级汇报”,把责任踢给自己的调用者。如果你违反了这一点,就有可能采用一种不负责任,危险的写法:

public String foo() {
String found = find();
if (found == null) {
return null;
}
}

当看到find()返回了null,foo自己也返回null。这样null就从一个地方,游走到了另一个地方,而且它表示另外一个意思。如果你不假思索就写出这样的代码,最后的结果就是代码里面随时随地都可能出现null。到后来为了保护自己,你的每个函数都会写成这样:

public void foo(A a, B b, C c) {
if (a == null) { … }
if (b == null) { … }
if (c == null) { … }

}

- 函数作者:明确声明不接受null参数,当参数是null时立即崩溃。不要试图对null进行“容错”,不要让程序继续往下执行。如果调用者使用了null作为参数,那么调用者(而不是函数作者)应该对程序的崩溃负全责。

上面的例子之所以成为问题,就在于人们对于null的“容忍态度”。这种“保护式”的写法,试图“容错”,试图“优雅的处理null”,其结果是让调用者更加肆无忌惮的传递null给你的函数。到后来,你的代码里出现一堆堆nonsense的情况,null可以在任何地方出现,都不知道到底是哪里产生出来的。谁也不知道出现了null是什么意思,该做什么,所有人都把null踢给其他人。最后这null像瘟疫一样蔓延开来,到处都是,成为一场噩梦。

正确的做法,其实是强硬的态度。你要告诉函数的使用者,我的参数全都不能是null,如果你给我null,程序崩溃了该你自己负责。至于调用者代码里有null怎么办,他自己该知道怎么处理(参考以上几条),不应该由函数作者来操心。

采用强硬态度一个很简单的做法是使用`Objects.requireNonNull()`。它的定义很简单:

public static <T> T requireNonNull(T obj) {
if (obj == null) {
throw new NullPointerException();
} else {
return obj;
}
}

你可以用这个函数来检查不想接受null的每一个参数,只要传进来的参数是null,就会立即触发`NullPointerException`崩溃掉,这样你就可以有效地防止null指针不知不觉传递到其它地方去。

- 使用@NotNull和@Nullable标记。IntelliJ提供了@NotNull和@Nullable两种标记,加在类型前面,这样可以比较简洁可靠地防止null指针的出现。IntelliJ本身会对含有这种标记的代码进行静态分析,指出运行时可能出现`NullPointerException`的地方。在运行时,会在null指针不该出现的地方产生`IllegalArgumentException`,即使那个null指针你从来没有deference。这样你可以在尽量早期发现并且防止null指针的出现。

- 使用Optional类型。Java 8和Swift之类的语言,提供了一种叫Optional的类型。正确的使用这种类型,可以在很大程度上避免null的问题。null指针的问题之所以存在,是因为你可以在没有“检查”null的情况下,“访问”对象的成员。

Optional类型的设计原理,就是把“检查”和“访问”这两个操作合二为一,成为一个“原子操作”。这样你没法只访问,而不进行检查。这种做法其实是ML,Haskell等语言里的模式匹配(pattern matching)的一个特例。模式匹配使得类型判断和访问成员这两种操作合二为一,所以你没法犯错。

比如,在Swift里面,你可以这样写:

let found = find()
if let content = found {
print(“found: ” + content)
}

你从`find()`函数得到一个Optional类型的值`found`。假设它的类型是`String?`,那个问号表示它可能包含一个String,也可能是nil。然后你就可以用一种特殊的if语句,同时进行null检查和访问其中的内容。这个if语句跟普通的if语句不一样,它的条件不是一个Bool,而是一个变量绑定`let content = found`。

我不是很喜欢这语法,不过这整个语句的含义是:如果found是nil,那么整个if语句被略过。如果它不是nil,那么变量content被绑定到found里面的值(unwrap操作),然后执行`print("found: " + content)`。由于这种写法把检查和访问合并在了一起,你没法只进行访问而不检查。

Java 8的做法比较蹩脚一些。如果你得到一个Optional<String>类型的值found,你必须使用“函数式编程”的方式,来写这之后的代码:

Optional<String> found = find();
found.ifPresent(content -> System.out.println(“found: ” + content));

这段Java代码跟上面的Swift代码等价,它包含一个“判断”和一个“取值”操作。ifPresent先判断found是否有值(相当于判断是不是null)。如果有,那么将其内容“绑定”到lambda表达式的content参数(unwrap操作),然后执行lambda里面的内容,否则如果found没有内容,那么ifPresent里面的lambda不执行。

Java的这种设计有个问题。判断null之后分支里的内容,全都得写在lambda里面。在函数式编程里,这个lambda叫做“[continuation](https://en.wikipedia.org/wiki/Continuation)”,Java把它叫做
“[Consumer](https://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html)”,它表示“如果found不是null,拿到它的值,然后应该做什么”。由于lambda是个函数,你不能在里面写`return`语句返回出外层的函数。比如,如果你要改写下面这个函数(含有null):

public static String foo() {
String found = find();
if (found != null) {
return found;
} else {
return “”;
}
}

就会比较麻烦。因为如果你写成这样:

public static String foo() {
Optional<String> found = find();
found.ifPresent(content -> {
return content; // can’t return from foo here
});
return “”;
}

里面的`return a`,并不能从函数`foo`返回出去。它只会从lambda返回,而且由于那个lambda([Consumer.accept](https://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html#accept-T-))的返回类型必须是`void`,编译器会报错,说你返回了String。由于Java里closure的自由变量是只读的,你没法对lambda外面的变量进行赋值,所以你也不能采用这种写法:

public static String foo() {
Optional<String> found = find();
String result = “”;
found.ifPresent(content -> {
result = content; // can’t assign to result
});
return result;
}

所以,虽然你在lambda里面得到了found的内容,如何使用这个值,如何返回一个值,却让人摸不着头脑。你平时的那些Java编程手法,在这里几乎完全废掉了。实际上,判断null之后,你必须使用Java 8提供的一系列古怪的[函数式编程操作](http://www.oracle.com/technetwork/articles/java/java8-optional-2175753.html):`map`, `flatMap`, `orElse`之类,想法把它们组合起来,才能表达出原来代码的意思。比如之前的代码,只能改写成这样:

public static String foo() {
Optional<String> found = find();
return found.orElse(“”);
}

这简单的情况还好。复杂一点的代码,我还真不知道怎么表达,我怀疑Java 8的Optional类型的方法,到底有没有提供足够的表达力。那里面少数几个东西表达能力不咋的,论工作原理,却可以扯到functor,continuation,甚至monad等高深的理论…… 仿佛用了Optional之后,这语言就不再是Java了一样。

所以Java虽然提供了Optional,但我觉得可用性其实比较低,难以被人接受。相比之下,Swift的设计更加简单直观,接近普通的过程式编程。你只需要记住一个特殊的语法`if let content = found {...}`,里面的代码写法,跟普通的过程式语言没有任何差别。

总之你只要记住,使用Optional类型,要点在于“原子操作”,使得null检查与取值合二为一。这要求你必须使用我刚才介绍的特殊写法。如果你违反了这一原则,把检查和取值分成两步做,还是有可能犯错误。比如在Java 8里面,你可以使用`found.get()`这样的方式直接访问found里面的内容。在Swift里你也可以使用`found!`来直接访问而不进行检查。

你可以写这样的Java代码来使用Optional类型:

Option<String> found = find();
if (found.isPresent()) {
System.out.println(“found: ” + found.get());
}

如果你使用这种方式,把检查和取值分成两步做,就可能会出现运行时错误。`if (found.isPresent())`本质上跟普通的null检查,其实没什么两样。如果你忘记判断`found.isPresent()`,直接进行`found.get()`,就会出现`NoSuchElementException`。这跟`NullPointerException`本质上是一回事。所以这种写法,比起普通的null的用法,其实换汤不换药。如果你要用Optional类型而得到它的益处,请务必遵循我之前介绍的“原子操作”写法。


### 防止过度工程

人的脑子真是奇妙的东西。虽然大家都知道过度工程(over-engineering)不好,在实际的工程中却经常不由自主的出现过度工程。我自己也犯过好多次这种错误,所以觉得有必要分析一下,过度工程出现的信号和兆头,这样可以在初期的时候就及时发现并且避免。

过度工程即将出现的一个重要信号,就是当你过度的思考“将来”,考虑一些还没有发生的事情,还没有出现的需求。比如,“如果我们将来有了上百万行代码,有了几千号人,这样的工具就支持不了了”,“将来我可能需要这个功能,所以我现在就把代码写来放在那里”,“将来很多人要扩充这片代码,所以现在我们就让它变得可重用”……

这就是为什么很多软件项目如此复杂。实际上没做多少事情,却为了所谓的“将来”,加入了很多不必要的复杂性。眼前的问题还没解决呢,就被“将来”给拖垮了。人们都不喜欢目光短浅的人,然而在现实的工程中,有时候你就是得看近一点,把手头的问题先搞定了,再谈以后扩展的问题。

另外一种过度工程的来源,是过度的关心“代码重用”。很多人“可用”的代码还没写出来呢,就在关心“重用”。为了让代码可以重用,最后被自己搞出来的各种框架捆住手脚,最后连可用的代码就没写好。如果可用的代码都写不好,又何谈重用呢?很多一开头就考虑太多重用的工程,到后来被人完全抛弃,没人用了,因为别人发现这些代码太难懂了,自己从头开始写一个,反而省好多事。

过度地关心“测试”,也会引起过度工程。有些人为了测试,把本来很简单的代码改成“方便测试”的形式,结果引入很多复杂性,以至于本来一下就能写对的代码,最后复杂不堪,出现很多bug。

世界上有两种“没有bug”的代码。一种是“没有明显的bug的代码”,另一种是“明显没有bug的代码”。第一种情况,由于代码复杂不堪,加上很多测试,各种coverage,貌似测试都通过了,所以就认为代码是正确的。第二种情况,由于代码简单直接,就算没写很多测试,你一眼看去就知道它不可能有bug。你喜欢哪一种“没有bug”的代码呢?

根据这些,我总结出来的防止过度工程的原则如下:

1. 先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
2. 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
3. 先写出可用,简单,明显没有bug的代码,再考虑测试的问题。

> 创造这样的精品文章需要很多的精力和咖啡 ;) 如果你喜欢这篇文章,请付款支持。建议金额$5美元或者30人民币。付款方式请参考[这里](http://www.yinwang.org/blog-cn/2016/04/13/pay-blog)。

相关文章

No Comments, Be The First!
近期评论
    功能
    网站地图xml地图