lucida.me - RSS









Search Preview

lucida

lucida.me

.me > lucida.me

SEO audit: Content analysis

Language Error! No language localisation is found.
Title lucida
Text / HTML ratio 47 %
Frame Excellent! The website does not use iFrame solutions.
Flash Excellent! The website does not have any flash contents.
Keywords cloud = > return lambda public Java => == int >> scope String iScheme args static list SObject SExpression
Keywords consistency
Keyword Content Title Description Headings
= 211
> 119
return 107
lambda 84
public 79
73
Headings Error! The website does not use (H) tags.
Images We found 0 images on this web page.

SEO Keywords (Single)

Keyword Occurrence Density
= 211 10.55 %
> 119 5.95 %
return 107 5.35 %
lambda 84 4.20 %
public 79 3.95 %
73 3.65 %
Java 61 3.05 %
=> 61 3.05 %
== 54 2.70 %
int 51 2.55 %
>> 47 2.35 %
scope 47 2.35 %
String 37 1.85 %
iScheme 34 1.70 %
args 34 1.70 %
static 32 1.60 %
list 30 1.50 %
SObject 28 1.40 %
SExpression 28 1.40 %
28 1.40 %

SEO Keywords (Two Word)

Keyword Occurrence Density
1 2 130 6.50 %
2 3 121 6.05 %
3 4 102 5.10 %
4 5 77 3.85 %
5 6 59 2.95 %
6 7 51 2.55 %
7 8 44 2.20 %
8 9 37 1.85 %
9 10 33 1.65 %
10 11 31 1.55 %
scope => 28 1.40 %
public static 28 1.40 %
args scope 26 1.30 %
Java 8 26 1.30 %
11 12 26 1.30 %
a b 23 1.15 %
12 13 21 1.05 %
firstValue == 18 0.90 %
= new 18 0.90 %
13 14 17 0.85 %

SEO Keywords (Three Word)

Keyword Occurrence Density Possible Spam
1 2 3 120 6.00 % No
2 3 4 100 5.00 % No
3 4 5 77 3.85 % No
4 5 6 59 2.95 % No
5 6 7 51 2.55 % No
6 7 8 44 2.20 % No
7 8 9 37 1.85 % No
8 9 10 33 1.65 % No
9 10 11 31 1.55 % No
10 11 12 26 1.30 % No
args scope => 26 1.30 % No
11 12 13 21 1.05 % No
12 13 14 17 0.85 % No
13 14 15 16 0.80 % No
if firstValue == 15 0.75 % No
14 15 16 13 0.65 % No
else if firstValue 12 0.60 % No
15 16 17 11 0.55 % No
Java SE 8 10 0.50 % No
17 18 19 9 0.45 % No

SEO Keywords (Four Word)

Keyword Occurrence Density Possible Spam
1 2 3 4 99 4.95 % No
2 3 4 5 77 3.85 % No
3 4 5 6 59 2.95 % No
4 5 6 7 51 2.55 % No
5 6 7 8 44 2.20 % No
6 7 8 9 37 1.85 % No
7 8 9 10 33 1.65 % No
8 9 10 11 31 1.55 % No
9 10 11 12 26 1.30 % No
10 11 12 13 21 1.05 % No
11 12 13 14 17 0.85 % No
12 13 14 15 16 0.80 % No
13 14 15 16 13 0.65 % No
else if firstValue == 12 0.60 % No
14 15 16 17 11 0.55 % No
BuildIn args scope => 9 0.45 % No
15 16 17 18 9 0.45 % No
16 17 18 19 9 0.45 % No
17 18 19 20 8 0.40 % No
18 19 20 21 8 0.40 % No

Internal links in - lucida.me

归档
归档 | lucida
分类
categories | lucida
标签
tags | lucida
plog
分类: plog | lucida
阅读全文 »
摄影手记 1 | lucida
blog
分类: blog | lucida
阅读全文 »
坚毅(GRIT)阅读笔记 | lucida
lucida.me/notes
抱歉,找不到这个页面... | lucida
编程
分类: 编程 | lucida
深入理解 Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法) | lucida
深入理解 Java 8 Lambda(类库篇——Streams API,Collector 和并行)
抱歉,找不到这个页面... | lucida
深入浅出Java 8 Lambda(语言篇)
深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法) | lucida
阅读全文 »
深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行) | lucida
2
lucida
3
lucida
9 分类
categories | lucida
26 标签
tags | lucida
RSS
lucida

Lucida.me Spined HTML


lucida BE DISCIPLINED 2018-07-10T05:55:24.987Z http://lucida.me/ lucida Hexo 摄影手记 1 http://lucida.me//blog/photolog-1/ 2018-07-06T23:29:59.000Z 2018-07-10T05:55:24.987Z 从去年年中间开始,机缘巧合下搞了一台还算不错的相机(SONY RX100V),本来是打算买来给 LP 拍照玩的,但 LP 嫌学习拍照麻烦于是我就自己先学了一遍然后再教老婆,没想到学习的过程中对拍照产生了兴趣。于是又搞了一台可换镜头相机(Interchangable Lens Camera)富士 X-T20 和几个变焦镜头,然后在周末一边给 LP 拍照一边学习拍照,到现在拍了快一万张了。然而这些照片都躺在硬盘里,有时 LP 会拿这些照片发朋友圈,但我总觉得哪里不对劲:1. 朋友圈并不是一个分享照片的好地方;2. 拍了大量的照片却没有反思当时拍这些照片的心路历程以至于水平一直上不去;3. 照片和文字在一起才能形成故事(Story),加上 4. 博客吃灰。我决定写一个不限长的摄影手记连载,记录我拍下的照片,以及拍照片前后的故事。设备在开篇简单聊一下我的设备:相机:Fujifilm X-T20:操控很好的中价位可换镜头无反相机,画质不错,体积也很小,操控极好,适合喜欢摄影的人。Sony RX100 V:便携相机之王,追焦连拍能力超强。镜头:XF1024, XF1855, XF18135, 和 XF55200覆盖 15-300 等效 35mm 焦段,从超广角到长焦都有覆盖都不是特别快(Fast Aperture)的镜头,但都够用都带有光学防抖,所以可以把快门拉慢 3 到 5 档都是变焦镜头,因为定焦镜头太烧钱且玄学成分太多都不是长枪大炮,不然就失去了无反相机便携的优势,此外我既不需要打鸟,也不用大 Bokeh配件:4 块配电Sandisk 高速 64GB SD 卡两张PeakDiamond出品的 Cuff 和 CaptureGorilla Pod 也就是国内山寨章鱼三脚架的原型大小不等的几个相机包(取决于我带几个镜头)后期:Adobe Lightroom Classic CC总之都是比较普通的设备,但算下来也要 5000 刀,可见摄影是一个多么烧钱的爱好。除非真的想学习摄影,否则用手机足矣,买几万块的相机只用自动挡打鸟拍花我觉得还是太奢侈了些。学习资源学习摄影我并没有看太多书:读了好几遍 X-T20 和 RX100 V 说明书读了两遍 Understanding Exposure 学习如何做正确的曝光读了一遍 The Photographer’s Eye 学习怎样构图接下来就是在 Flickr 和 500px 上看照片,记下这些照片的拍摄时间/地点/参数,然后自己参考模仿;此外加入了 Facebook 的 Fuji-X Users Group,与一众富士用户交流使用经验;最后就是 Youtube 和 Lynda 上的摄影视频教程,当然还有 Adobe 官方的 Lightroom 视频教程。]]> <p>从去年年中间开始,机缘巧合下搞了一台还算不错的相机(SONY RX100V),本来是打算买来给 LP 拍照玩的,但 LP 嫌学习拍照麻烦于是我就自己先学了一遍然后再教老婆,没想到学习的过程中对拍照产生了兴趣。于是又搞了一台可换镜头相机(Interchangable Lens Camera)富士 X-T20 和几个变焦镜头,然后在周末一边给 LP 拍照一边学习拍照,到现在拍了快一万张了。</p> <p>然而这些照片都躺在硬盘里,有时 LP 会拿这些照片发朋友圈,但我总觉得哪里不对劲:1. 朋友圈并不是一个分享照片的好地方;2. 拍了大量的照片却没有反思当时拍这些照片的心路历程以至于水平一直上不去;3. 照片和文字在一起才能形成故事(Story),加上 4. 博客吃灰。我决定写一个不限长的摄影手记连载,记录我拍下的照片,以及拍照片前后的故事。</p> <p><img src="/images/plog/1/museum.jpg" alt="Shanghai Museum"></p> <h2 id="设备"><a href="#设备" class="headerlink" title="设备"></a>设备</h2><p>在开篇简单聊一下我的设备:</p> <p>相机:</p> <ul> <li>Fujifilm X-T20:操控很好的中价位可换镜头无反相机,画质不错,体积也很小,操控极好,适合喜欢摄影的人。</li> <li>Sony RX100 V:便携相机之王,追焦连拍能力超强。</li> </ul> <p>镜头:</p> <ul> <li>XF1024, XF1855, XF18135, 和 XF55200</li> <li>覆盖 15-300 等效 35mm 焦段,从超广角到长焦都有覆盖</li> <li>都不是特别快(Fast Aperture)的镜头,但都够用</li> <li>都带有光学防抖,所以可以把快门拉慢 3 到 5 档</li> <li>都是变焦镜头,因为定焦镜头太烧钱且玄学成分太多</li> <li>都不是长枪大炮,不然就失去了无反相机便携的优势,此外我既不需要打鸟,也不用大 Bokeh</li> </ul> 坚毅(GRIT)阅读笔记 http://lucida.me//blog/Grit-read-notes/ 2018-01-29T22:29:58.000Z 2018-07-10T05:55:24.987Z 图书简介宾夕法尼亚大学心理学副教授 Angela Duckworth 通过对成功人士进行研究,得出“坚毅”是成功人士所具备的关键素质,并通过其对坚毅的研究获得麦克阿瑟天才奖。Grit (中文版 坚毅:释放激情与坚持的力量) 这本书记录了她的研究成果:坚毅和成功存在正向相关什么是坚毅如何培养坚毅亮点如何测量自己的坚毅值天赋 X 努力 = 技能;技能 X 努力 = 成就打通了 刻意练习,心流 和 成长心态 这三个概念之间的联系如何建立长期(高级)目标,并通过长期目标培养坚毅如何培养激情如何教育孩子培养坚毅(我没有读这部分,暂时用不到)章节简介坚毅值能够准确的预测西点军校新生是否能坚持通过野兽训练营成功不仅仅需要天赋人们偏好天才努力也很重要天赋 X 努力 = 技能;技能 X 努力 = 成就如何测试坚毅指数坚毅指在很长的一段时间持续追求同一个顶级目标设定顶级目标,然后将中低级目标与其相连坚毅可以通过后天塑造兴趣,练习,目的,希望对一件事拥有持久的兴趣,以培养激情通过刻意练习,获得心流体验追求内心使命的召唤,而非感官享乐学会应对失败,提高对痛苦的抗性语录决心和方向:不管身处哪个领域,高成就者都怀有一种相当惊人的决心:更多的韧性与勤奋明确的知道自己要什么优异表现的由来:优异的表现是几十个技能或小活动的汇聚,这些技能或活动是习得的或偶然悟到的,经过认真锤炼,成为习惯,然后且合在一起成为一个综合的整体对天才的崇拜:我们的虚荣和自恋促成了对天才的崇拜。因为如果我们认为才能是一种神奇的东西,我们就没有必要与他人相比较,从而发现自己的不足…… 成某人有天分的意思是,你没有必要与他竞争天赋,技能,努力天赋:当你投入努力的时候,你的技能能提升多快努力:当你努力运用技能时所产生的结果坚毅坚毅指在很长的一段时间持续追求同一个顶级目标坚毅可以把非睡眠时间组织起来坚毅的人的中低级目标会与顶级目标相连激情激情不会顿悟,而是需要积极地去发展刻意练习定义清晰的延展性目标全神贯注及不懈努力即时的、有益的反馈持续的反思和完善培养目标感反思自己,如何对社会做出积极的状态采取微小但有意义的方式,改变当前工作态度,使其与自己的核心价值观> 更为紧密的联结从有目标感的楷模身上寻找激励总结同畅销概念书类似,这本书的套路还是:坚毅很有用。为什么有用?因为案例 A, B, C 证明坚毅很有用坚毅是遗传的吗?不全是,可以培养怎么培养?通过 W, X, Y, Z (建立目标,刻意学习,进入心流,成长心态) 培养(其实把坚毅换成其它什么概念进来都差不多)对我来说,这本书最大的意义在于让我意识到自己的坚毅值很低,并串联了 刻意练习,心流 和 成长心态 等概念来改善坚毅值。此外这本书对长期目标,习得性无助,以及坚毅之间关系的论述也很有趣形象。总之,如果你了解 A: 刻意学习,心流,成长心态这些概念,并且 B: 自律有目标,那么这本书对你意义不大,可以直接跳过。但是,如果你不同时具备 A 和 B,那么这本书值得一读。延伸阅读刻意练习心流:最优体验心理学 终身成长:重新定义成功的思维模式]]> <h2 id="图书简介"><a href="#图书简介" class="headerlink" title="图书简介"></a>图书简介</h2><p><a href="https://amazon.cn/gp/product/B071HLZWG6/ref=as_li_tl?ie=UTF8&amp;camp=536&amp;creative=3200&amp;creativeASIN=B071HLZWG6&amp;linkCode=as2&amp;tag=lucida-23&amp;linkId=d7af67ba8d6765243a421c4ea285716d"><img src="/images/covers/grit_book.jpg" alt="Grid Book"></a></p> <p>宾夕法尼亚大学心理学副教授 Angela Duckworth 通过对成功人士进行研究,得出“坚毅”是成功人士所具备的关键素质,并通过其对坚毅的研究获得麦克阿瑟天才奖。<a href="https://amazon.cn/gp/product/1501144162/ref=as_li_tl?ie=UTF8&amp;camp=536&amp;creative=3200&amp;creativeASIN=1501144162&amp;linkCode=as2&amp;tag=lucida-23&amp;linkId=f68e00f2971e69c1906da6012a8a3dae">Grit</a> (中文版 <a href="https://amazon.cn/gp/product/B071HLZWG6/ref=as_li_tl?ie=UTF8&amp;camp=536&amp;creative=3200&amp;creativeASIN=B071HLZWG6&amp;linkCode=as2&amp;tag=lucida-23&amp;linkId=d7af67ba8d6765243a421c4ea285716d">坚毅:释放激情与坚持的力量</a>) 这本书记录了她的研究成果:</p> <ol> <li>坚毅和成功存在正向相关</li> <li>什么是坚毅</li> <li>如何培养坚毅</li> </ol> <h2 id="亮点"><a href="#亮点" class="headerlink" title="亮点"></a>亮点</h2><ol> <li>如何测量自己的坚毅值</li> <li>天赋 X 努力 = 技能;技能 X 努力 = 成就</li> <li>打通了 <a href="https://amazon.cn/gp/product/B01MDQ7RAX/ref=as_li_tl?ie=UTF8&amp;camp=536&amp;creative=3200&amp;creativeASIN=B01MDQ7RAX&amp;linkCode=as2&amp;tag=lucida-23&amp;linkId=e8add125a6302a7cdd7b61924a641ae3">刻意练习</a>,<a href="https://amazon.cn/gp/product/B0772BTKQT/ref=as_li_tl?ie=UTF8&amp;camp=536&amp;creative=3200&amp;creativeASIN=B0772BTKQT&amp;linkCode=as2&amp;tag=lucida-23&amp;linkId=5ea7c4d3799e38809cfa1675c37bff69">心流</a> 和 <a href="https://amazon.cn/gp/product/B075YLTFM1/ref=as_li_tl?ie=UTF8&amp;camp=536&amp;creative=3200&amp;creativeASIN=B075YLTFM1&amp;linkCode=as2&amp;tag=lucida-23&amp;linkId=9781b7460881638a11e73882aff407e7">成长心态</a> 这三个概念之间的联系</li> <li>如何建立长期(高级)目标,并通过长期目标培养坚毅</li> <li>如何培养激情</li> <li>如何教育孩子培养坚毅(我没有读这部分,暂时用不到)</li> </ol> 2017 年的新技能(1) 杠铃 http://lucida.me//blog/2017-learned-barbell/ 2018-01-19T22:30:00.000Z 2018-07-10T05:55:24.987Z 从 2018 年开始,我打算继续写博客,不过不会写之前那样的长篇大论,毕竟时间投入太大。但也不太想写毫无信息量的文章,因为无论对自己对读者都没有价值。想了下,就从自己在 2017 年学会的新技能开始写吧。简单的写下自己学习的过程以及收获。没错,2017 年我学会的新技能之一是杠铃,而不是什么新语言或是新框架。通过杠铃:体重: 130 磅 -> 165 磅(一多半肌肉)深蹲: 65 磅 -> 230 磅硬拉: 95 磅 -> 300 磅卧推: 105 磅 -> 170 磅推举: 45 磅 -> 115 磅力量翻: 55 磅 -> 140 磅下面记录下我的学习/练习历程杠铃之前我在工作之前就已经很胖,而且是虚胖:没有力量,一身肥肉,胸围腰围臀围逐层递增的正三角。工作之后在 Google 更是管不住自己的嘴,从 140 斤一路涨到近 170 斤,如同浮肿一般。自己意识到这样胖下去迟早要出问题,加上在 Google 工作很清闲,于是我从 2015 年 8 月开始减肥。通过 哑铃 + 跳操(T25 和 Insantiy + 控制饮食,我在 15 年年底之前减到 130 斤。之后又通过每天骑行 20 公里 + 壶铃 + 自重训练,在 16 年年底把体重减到 120 斤。50 斤的变化非常巨大,以至于我后来办签证时都被质疑照片是不是本人。减肥的成功带来了另一个问题:我从一个极端走到了另一个极端。肥胖肯定不是好事,但过瘦也不是什么好事,于是从 2017 年开始,我开始尝试通过力量训练,增加净重(lean mass),增加力量。选择杠铃在杠铃之前,我先后尝试过:哑铃壶铃弹力带力量操单双杠训练但效果并不理想,这些训练提升了我的力量,但并没有提升多少。我在 16 年全年都在练哑铃,然而我的哑铃卧推还是 130 磅,二头肌弯举还是 30 磅。(均为 1 次最大重量)蛋白粉也吃了不少,然而一年过去力量毫无变化。于是我开始尝试杠铃。之所以一直没有练杠铃,是因为杠铃太占地,而公司的健身房人又太多,经常找不到位置。但考虑到已经没有别的选择,加上在 Youtube 上看健身的视频时发现他们对杠铃都是推崇备至(尤其是 Powerlifter),于是不太情愿的开始学习杠铃。学习杠铃下面是我学习杠铃的途径:观看 Youtube 的教学视频向公司健身房的私教学习基础动作(硬拉,深蹲,卧推,推举,力量翻) 2.1. 这里要赞一下 Google 北京的健身房教练,他在国家举重队实习过,非常了解纲领从书中学习如何使用杠铃,参考书籍是 Starting Strength 和 Practical Programming for Strength Training,前者用来学习动作,后者用来制定训练计划每周大重量练习三次,小重量天天练习练习计划每周一,三,五练习,练习计划非常简单:训练 A:深蹲:3 组热身,3 组最大重量卧推:2 组热身,3 组最大重量硬拉:2 组热身,1 组最大重量训练 B:深蹲:3 组热身,3 组最大重量推举:2 组热身,3 组最大重量引体向上:1 组热身,3 组做至力竭训练 C:深蹲:3 组热身,3 组最大重量卧推:2 组热身,3 组最大重量力量翻:2 组热身,3 组最大重量训练 D:深蹲:3 组热身,3 组最大重量推举:2 组热身,3 组最大重量引体向上:1 组热身,3 组做至力竭训练按照 A -> B -> C -> D 的顺序,如此往复热身组组间休息 1 分钟,大重量组组间休息 3 分钟每组做 5 次这个练习计划极其简单:每次都可以在 60 分钟内完成动作非常少(只有六个动作),因此不需要去学其它什么花哨的动作只需要杠铃、直凳、和深蹲架,因此不需要频繁的换设备(健身房里换设备很麻烦)营养补充训练的同时营养也要跟上,Starting Strength 一书极力推崇牛奶 + 蛋白粉。我一天大约要喝 2 公斤牛奶 + 50 克蛋白粉,分六次“服用”。以至于一段时间同事都提到我身上奶香四溢。除此之外我每天吃一粒维生素片和鱼油片,以补充微量元素/维生素/Omega-3脂肪酸。其它就是正常饮食,低糖低油高蛋白练习效果尽管训练计划很简单,但是效果很好,以深蹲为例,在最初的六周,每次深蹲我都可以在之前的基础上增加 5 磅重量。从 17 年 3 月开始练习,到现在(一共 9 个月):体重: 130 磅 -> 165 磅(一多半肌肉)深蹲: 65 磅 -> 230 磅硬拉: 95 磅 -> 300 磅卧推: 105 磅 -> 170 磅推举: 45 磅 -> 115 磅力量翻: 55 磅 -> 140 磅最后附图两张:正在练习推举 105 磅的自己:和我一起训练的 LP,腹肌已经若隐若现(以及今年购入的 Rogue 全套设备,以后就可以在家练习了)]]> <p>从 2018 年开始,我打算继续写博客,不过不会写之前那样的长篇大论,毕竟时间投入太大。但也不太想写毫无信息量的文章,因为无论对自己对读者都没有价值。想了下,就从自己在 2017 年学会的新技能开始写吧。简单的写下自己学习的过程以及收获。</p> <p>没错,2017 年我学会的新技能之一是杠铃,而不是什么新语言或是新框架。通过杠铃:</p> <ul> <li>体重: 130 磅 -&gt; 165 磅(一多半肌肉)</li> <li>深蹲: 65 磅 -&gt; 230 磅</li> <li>硬拉: 95 磅 -&gt; 300 磅</li> <li>卧推: 105 磅 -&gt; 170 磅</li> <li>推举: 45 磅 -&gt; 115 磅</li> <li>力量翻: 55 磅 -&gt; 140 磅</li> </ul> <p>下面记录下我的学习/练习历程</p> <h2 id="杠铃之前"><a href="#杠铃之前" class="headerlink" title="杠铃之前"></a>杠铃之前</h2><p>我在工作之前就已经很胖,而且是虚胖:没有力量,一身肥肉,胸围腰围臀围逐层递增的正三角。工作之后在 Google 更是管不住自己的嘴,从 140 斤一路涨到近 170 斤,如同浮肿一般。</p> <p>自己意识到这样胖下去迟早要出问题,加上在 Google 工作很清闲,于是我从 2015 年 8 月开始减肥。通过 哑铃 + 跳操(T25 和 Insantiy + 控制饮食,我在 15 年年底之前减到 130 斤。之后又通过每天骑行 20 公里 + 壶铃 + 自重训练,在 16 年年底把体重减到 120 斤。50 斤的变化非常巨大,以至于我后来办签证时都被质疑照片是不是本人。</p> <p>减肥的成功带来了另一个问题:我从一个极端走到了另一个极端。肥胖肯定不是好事,但过瘦也不是什么好事,于是从 2017 年开始,我开始尝试通过力量训练,增加净重(lean mass),增加力量。</p> 2018,从对自己诚实开始 http://lucida.me//blog/2018-being-honest/ 2018-01-07T22:47:42.000Z 2018-07-10T05:55:24.987Z 警告:意识流文章。很久没有写过新文章了,无论是 lucida.me 还是 lucida.me/notes,上次更新的文章都尴尬的定格在 2016 年。而自己的 微博 更是成为失踪人口,从 2017 年总共发出 3 条微博。从最初在博客园上写博客 figure9 到后来的自建博客 lucida,已经过去了十一年,自己也从当年虎逼哄哄的小本科生变成了现在的中年油腻男子。现在回看以前自己的博客,悲哀的发现自己已远没有当年对技术的热情,或是对人生目标的清晰——上学时自己的想法很单纯——写最好的程序,然后以此作为基石找到一份好工作。现在看这个目标是实现了:找到了一个对应届生而言还不错的工作,自己还写了一篇现在看起来十分可笑的 心路历程,当时那篇文章被各种转发,自己的虚荣心也得到了极大满足。然而那篇文章似乎成为了一个诅咒——在那篇文章后,在工作上,我再也没有任何突破。在技术上,也许我还不如 5 年的自己,在公司里,我在 Google 换了三次组:先是做了接近两年自动化测试,做到后来我自己都鄙视我自己,于是换组做 Android 应用开发,刚刚找到一点感觉,然后项目被砍,于是换到另一个组,做了一年之后项目又被砍。我在 Google 待了 3 年,始终停留在最初的入门级别,看着一起入职的同事纷纷成为组里的 Tech Lead,而自己却仍然在做一些入门的工作,我意识到自己当初选择 Google 是个错误的决定——它仅仅满足了我当时的虚荣心。而我在工作之后,对职业发展没有任何概念,也没有任何目标,于是就浑浑噩噩的在 Google 混了 3 年。可笑的是,我在 Google 里工作不顺,并没有自己想办法改变现状,而是在工作以外的地方寻找慰藉,以填补自己的虚荣心。在这段时间,我创建了自己的独立博客 lucida.me,并花费大量业余时间撰写了诸如 Sublime 教程和程序员必读书单之类的质量长文。这些文章的反响很好,我因为这些文章也成为了读者口中的大牛。虚荣心再次得到满足。然而与网络的“成名”相比,自己在现实工作中毫无进展。自己在 Google 3 年始终停留在入门级别(一般来说,正常表现一年半到两年就可以升一级),尽管有换组和项目被砍这些因素,但无可否认,自己前三年的工作(也许是最宝贵的三年),彻底的 doomed。在 Google 时有不少人通过我的博客找到我,想和我约饭聊天(他们可能认为我真的是大牛吧),都被我自己的偷偷的躲掉了——我并不想让他们看到现实中的自己。2016 年年初,我在某个中国员工的介绍下进入了 Google Fiber,做一些内部工具,当时我打算利用这个机会好好做些东西——然后升一级,然而在 16 年下半年,Fiber 业务不善,大量高管离职或是换岗,这时我意识到我有两个选择:换组,在 Google 继续浑浑噩噩的做下去。稳定,因为只要没有大错,Google 不会裁人。换公司。不稳定,可能会面临没工作的境地。我有想过选择 1,但是内心的矛盾,以及在 Google 同事面前的尴尬,最终让我选择 2。与其在 Google 受人鄙视的工作下去,我宁可在其它公司,重新开始。于是我开始了找工作的历程,在同事朋友的推荐下,2017 年我进入了 FB,在 Google 浑浑噩噩的工作 3 年之后,我按下了重启按钮。也许这是一个错误的选择,但这至少是我自己的选择。2017 年整年我既没有写新文章,也极少更新微博状态:相比人浮于事的 Google,FB 显得务实很多——至少不会让你没有工作可做,所以我没有那么多时间写文章我不想再通过写博客获取关注度,来掩盖自己在现实中落魄的事实2017 这一年中,我在 FB 写了不少东西,也结识了不少新的朋友。读了一些书,也获得了一些新的技能(开车,摄影,举重)。最大的收获,是终于可以诚实的面对自己,不会因为自己在线上和线下的不一致而产生自我认知矛盾。Ray Dalio 在 The Principles 一书中提到要对自己诚实,要极度现实,要保持头脑极度开放。于是我写下这篇文章,希望它可以:化解我自己从 2014 年以来的自我认知矛盾对自己过去 3 年(2014-2016)失败的工作经历做一个总结总结 2017 年与其在网络上打扮成自己希望别人所看到的自己,不如对自己诚实,在现实中成为自己希望的自己。希望这篇文章可以终结我自己的浮夸,终结我 5 年前写的那篇文章的“诅咒”。Stay hungry, stay foolish, and be honest to myself。这是 18 年的第一篇文章,但不会是最后一篇。希望我可以通过写作,锻炼自己的思考能力,总结自己的收获,清晰自己的目标,让自己对自己更诚实。]]> <p>警告:意识流文章。</p> <p>很久没有写过新文章了,无论是 <a href="http://lucida.me">lucida.me</a> 还是 <a href="http://lucida.me/notes">lucida.me/notes</a>,上次更新的文章都尴尬的定格在 2016 年。而自己的 <a href="http://weibo.com/pegong/">微博</a> 更是成为失踪人口,从 2017 年总共发出 3 条微博。</p> <p>从最初在博客园上写博客 <a href="http://www.cnblogs.com/figure9">figure9</a> 到后来的自建博客 <a href="http://lucida.me">lucida</a>,已经过去了十一年,自己也从当年虎逼哄哄的小本科生变成了现在的中年油腻男子。现在回看以前自己的博客,悲哀的发现自己已远没有当年对技术的热情,或是对人生目标的清晰——上学时自己的想法很单纯——写最好的程序,然后以此作为基石找到一份好工作。现在看这个目标是实现了:找到了一个对应届生而言还不错的工作,自己还写了一篇现在看起来十分可笑的 <a href="http://www.cnblogs.com/figure9/archive/2013/01/09/2853649.html">心路历程</a>,当时那篇文章被各种转发,自己的虚荣心也得到了极大满足。</p> 深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行) http://lucida.me//blog/java-8-lambdas-inside-out-library-features/ 2016-09-27T21:39:29.000Z 2018-07-10T05:55:24.987Z 关于深入理解 Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)深入理解 Java 8 Lambda(类库篇——Streams API,Collector 和并行)深入理解 Java 8 Lambda(原理篇——Java 编译器如何处理 lambda)本文是深入理解 Java 8 Lambda 系列的第二篇,主要介绍 Java 8 针对新增语言特性而新增的类库(例如 Streams API、Collectors 和并行)。本文是对 Brian Goetz的State of the Lambda: Libraries Edition 一文的翻译。Java SE 8 增加了新的语言特性(例如 lambda 表达式和默认方法),为此 Java SE 8 的类库也进行了很多改进,本文简要介绍了这些改进。在阅读本文前,你应该先阅读 深入浅出Java 8 Lambda(语言篇),以便对 Java SE 8 的新增特性有一个全面了解。背景(Background)自从lambda表达式成为Java语言的一部分之后,Java集合(Collections)API就面临着大幅变化。而 JSR 355(规定了 Java lambda 表达式的标准)的正式启用更是使得 Java 集合 API 变的过时不堪。尽管我们可以从头实现一个新的集合框架(比如“Collection II”),但取代现有的集合框架是一项非常艰难的工作,因为集合接口渗透了 Java 生态系统的每个角落,将它们一一换成新类库需要相当长的时间。因此,我们决定采取演化的策略(而非推倒重来)以改进集合 API:为现有的接口(例如 Collection,List 和 Stream)增加扩展方法;在类库中增加新的 流(stream,即 java.util.stream.Stream)抽象以便进行聚集(aggregation)操作;改造现有的类型使之可以提供流视图(stream view);改造现有的类型使之可以容易的使用新的编程模式,这样用户就不必抛弃使用以久的类库,例如 ArrayList 和 HashMap(当然这并不是说集合 API 会常驻永存,毕竟集合 API 在设计之初并没有考虑到 lambda 表达式。我们可能会在未来的 JDK 中添加一个更现代的集合类库)。除了上面的改进,还有一项重要工作就是提供更加易用的并行(Parallelism)库。尽管 Java 平台已经对并行和并发提供了强有力的支持,然而开发者在实际工作(将串行代码并行化)中仍然会碰到很多问题。因此,我们希望 Java 类库能够既便于编写串行代码也便于编写并行代码,因此我们把编程的重点从具体执行细节(how computation should be formed)转移到抽象执行步骤(what computation should be perfomed)。除此之外,我们还需要在将并行变的 容易(easier)和将并行变的 不可见(invisible)之间做出抉择,我们选择了一个折中的路线:提供 显式(explicit)但 非侵入(unobstrusive)的并行。(如果把并行变的透明,那么很可能会引入不确定性(nondeterminism)以及各种数据竞争(data race)问题)内部迭代和外部迭代(Internal vs external iteration)集合类库主要依赖于 外部迭代(external iteration)。Collection 实现 Iterable 接口,从而使得用户可以依次遍历集合的元素。比如我们需要把一个集合中的形状都设置成红色,那么可以这么写:123for (Shape shape : shapes) { shape.setColor(RED);}这个例子演示了外部迭代:for-each 循环调用 shapes 的 iterator() 方法进行依次遍历。外部循环的代码非常直接,但它有如下问题:Java 的 for 循环是串行的,而且必须按照集合中元素的顺序进行依次处理;集合框架无法对控制流进行优化,例如通过排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。尽管有时 for-each 循环的这些特性(串行,依次)是我们所期待的,但它对改善性能造成了阻碍。我们可以使用 内部迭代(internal iteration)替代外部迭代,用户把对迭代的控制权交给类库,并向类库传递迭代时所需执行的代码。下面是前例的内部迭代代码:1shapes.forEach(s -> s.setColor(RED));尽管看起来只是一个小小的语法改动,但是它们的实际差别非常巨大。用户把对操作的控制权交还给类库,从而允许类库进行各种各样的优化(例如乱序执行、惰性求值和并行等等)。总的来说,内部迭代使得外部迭代中不可能实现的优化成为可能。外部迭代同时承担了 做什么(把形状设为红色)和 怎么做(得到 Iterator 实例然后依次遍历)两项职责,而内部迭代只负责 做什么,而把 怎么做 留给类库。通过这样的职责转变:用户的代码会变得更加清晰,而类库则可以进行各种优化,从而使所有用户都从中受益。流(Stream)流 是 Java SE 8 类库中新增的关键抽象,它被定义于 java.util.stream(这个包里有若干流类型:Stream<T> 代表对象引用流,此外还有一系列特化(specialization)流,比如 IntStream 代表整形数字流)。每个流代表一个值序列,流提供一系列常用的聚集操作,使得我们可以便捷的在它上面进行各种运算。集合类库也提供了便捷的方式使我们可以以操作流的方式使用集合、数组以及其它数据结构。流的操作可以被组合成 流水线(Pipeline)。以前面的例子为例,如果我们只想把蓝色改成红色:123shapes.stream() .filter(s -> s.getColor() == BLUE) .forEach(s -> s.setColor(RED));在Hodgepodge上调用 stream() 会生成该集合元素的流视图(stream view),接下来 filter() 操作会产生只包含蓝色形状的流,最后,这些蓝色形状会被 forEach 操作设为红色。如果我们想把蓝色的形状提取到新的 List 里,则可以:1234List<Shape> undecorous = shapes.stream() .filter(s -> s.getColor() == BLUE) .collect(Collectors.toList());collect() 操作会把其接收的元素聚集(aggregate)到一起(这里是 List),collect() 方法的参数则被用来指定如何进行聚集操作。在这里我们使用 toList() 以把元素输出到 List 中。(如需更多 collect() 方法的细节,请阅读 Collectors 一节)如果每个形状都被保存在 Box 里,然后我们想知道哪个盒子至少包含一个蓝色形状,我们可以这么写:12345Set<Box> hasBlueShape = shapes.stream() .filter(s -> s.getColor() == BLUE) .map(s -> s.getContainingBox()) .collect(Collectors.toSet());map() 操作通过映射函数(这里的映射函数接收一个形状,然后返回包含它的盒子)对输入流里面的元素进行依次转换,然后产生新流。如果我们需要得到蓝色物体的总重量,我们可以这样表达:12345int sum = shapes.stream() .filter(s -> s.getColor() == BLUE) .mapToInt(s -> s.getWeight()) .sum();这些例子演示了流框架的设计,以及如何使用流框架解决实际问题。流和集合(Streams vs Collections)集合和流尽管在表面上看起来很相似,但它们的设计目标是不同的:集合主要用来对其元素进行有效(effective)的管理和访问(access),而流并不支持对其元素进行直接操作或直接访问,而只支持通过声明式操作在其上进行运算然后得到结果。除此之外,流和集合还有一些其它不同:无存储:流并不存储值;流的元素源自数据源(可能是某个数据结构、生成函数或 I/O 通道等等),通过一系列计算步骤得到;天然的函数式风格(Functional in nature):对流的操作会产生一个结果,但流的数据源不会被修改;惰性求值:多数流操作(包括过滤、映射、排序以及去重)都可以以惰性方式实现。这使得我们可以用一遍遍历完成整个流水线操作,并可以用短路操作提供更高效的实现;无需上界(Bounds optional):不少问题都可以被表达为无限流(infinite stream):用户不停地读取流直到满意的结果出现为止(比如说,枚举 完美数 这个操作可以被表达为在所有整数上进行过滤)。集合是有限的,但流不是(操作无限流时我们必需使用短路操作,以确保操作可以在有限时间内完成);从API的角度来看,流和集合完全互相独立,不过我们可以既把集合作为流的数据源(Collection 拥有 stream() 和 parallelStream() 方法),也可以通过流产生一个集合(使用前例的 collect() 方法)。Collection 以外的类型也可以作为 stream 的数据源,比如JDK中的 BufferedReader、Random 和 BitSet 已经被改造可以用做流的数据源,Arrays.stream() 则产生给定数组的流视图。事实上,任何可以用 Iterator 描述的对象都可以成为流的数据源,如果有额外的信息(比如大小、是否有序等特性),库还可以进行进一步的优化。惰性(Laziness)过滤和映射这样的操作既可以被 急性求值(以 filter 为例,急性求值需要在方法返回前完成对所有元素的过滤),也可以被 惰性求值(用 Stream 代表过滤结果,当且仅当需要时才进行过滤操作)在实际中进行惰性运算可以带来很多好处。比如说,如果我们进行惰性过滤,我们就可以把过滤和流水线里的其它操作混合在一起,从而不需要对数据进行多遍遍历。相类似的,如果我们在一个大型集合里搜索第一个满足某个条件的元素,我们可以在找到后直接停止,而不是继续处理整个集合。(这一点对无限数据源是很重要,惰性求值对于有限数据源起到的是优化作用,但对无限数据源起到的是决定作用,没有惰性求值,对无限数据源的操作将无法终止)对于过滤和映射这样的操作,我们很自然的会把它当成是惰性求值操作,不过它们是否真的是惰性取决于它们的具体实现。另外,像 sum() 这样生成值的操作和 forEach() 这样产生副作用的操作都是“天然急性求值”,因为它们必须要产生具体的结果。以下面的流水线为例:12345int sum = shapes.stream() .filter(s -> s.getColor() == BLUE) .mapToInt(s -> s.getWeight()) .sum();这里的过滤操作和映射操作是惰性的,这意味着在调用 sum() 之前,我们不会从数据源提取任何元素。在 sum 操作开始之后,我们把过滤、映射以及求和混合在对数据源的一遍遍历之中。这样可以大大减少维持中间结果所带来的开销。大多数循环都可以用数据源(数组、集合、生成函数以及I/O管道)上的聚合操作来表示:进行一系列惰性操作(过滤和映射等操作),然后用一个急性求值操作(forEach,toArray 和 collect 等操作)得到最终结果——例如过滤—映射—累积,过滤—映射—排序—遍历等组合操作。惰性操作一般被用来计算中间结果,这在Streams API设计中得到了很好的体现——与其让 filter 和 map 返回一个集合,我们选择让它们返回一个新的流。在 Streams API 中,返回流对象的操作都是惰性操作,而返回非流对象的操作(或者无返回值的操作,例如 forEach())都是急性操作。绝大多数情况下,潜在的惰性操作会被用于聚合,这正是我们想要的——流水线中的每一轮操作都会接收输入流中的元素,进行转换,然后把转换结果传给下一轮操作。在使用这种 数据源—惰性操作—惰性操作—急性操作 流水线时,流水线中的惰性几乎是不可见的,因为计算过程被夹在数据源和最终结果(或副作用操作)之间。这使得API的可用性和性能得到了改善。对于 anyMatch(Predicate) 和 findFirst() 这些急性求值操作,我们可以使用短路(short-circuiting)来终止不必要的运算。以下面的流水线为例:1234Optional<Shape> firstBlue = shapes.stream() .filter(s -> s.getColor() == BLUE) .findFirst();由于过滤这一步是惰性的,findFirst 在从其上游得到一个元素之后就会终止,这意味着我们只会处理这个元素及其之前的元素,而不是所有元素。findFirst() 方法返回 Optional 对象,因为集合中有可能不存在满足条件的元素。Optional 是一种用于描述可缺失值的类型。在这种设计下,用户并不需要显式进行惰性求值,甚至他们都不需要了解惰性求值。类库自己会选择最优化的计算方式。并行(Parallelism)流水线既可以串行执行也可以并行执行,并行或串行是流的属性。除非你显式要求使用并行流,否则JDK总会返回串行流。(串行流可以通过 parallel() 方法被转化为并行流)尽管并行是显式的,但它并不需要成为侵入式的。利用 parallelStream(),我们可以轻松的把之前重量求和的代码并行化:12345int sum = shapes.parallelStream() .filter(s -> s.getColor = BLUE) .mapToInt(s -> s.getWeight()) .sum();并行化之后和之前的代码区别并不大,然而我们可以很容易看出它是并行的(此外我们并不需要自己去实现并行代码)。因为流的数据源可能是一个可变集合,如果在遍历流时数据源被修改,就会产生干扰(interference)。所以在进行流操作时,流的数据源应保持不变(held constant)。这个条件并不难维持,如果集合只属于当前线程,只要 lambda 表达式不修改流的数据源就可以。(这个条件和遍历集合时所需的条件相似,如果集合在遍历时被修改,绝大多数的集合实现都会抛出ConcurrentModificationException)我们把这个条件称为无干扰性(non-interference)。我们应避免在传递给流方法的 lambda 产生副作用。一般来说,打印调试语句这种输出变量的操作是安全的,然而在 lambda 表达式里访问可变变量就有可能造成数据竞争或是其它意想不到的问题,因为 lambda 在执行时可能会同时运行在多个线程上,因而它们所看到的元素有可能和正常的顺序不一致。无干扰性有两层含义:不要干扰数据源;不要干扰其它 lambda 表达式,当一个 lambda 在修改某个可变状态而另一个 lambda 在读取该状态时就会产生这种干扰。只要满足无干扰性,我们就可以安全的进行并行操作并得到可预测的结果,即便对线程不安全的集合(例如 ArrayList)也是一样。实例(Examples)下面的代码源自 JDK 中的Matriculation类型(getEnclosingMethod 方法),这段代码会遍历所有声明的方法,然后根据方法名称、返回类型以及参数的数量和类型进行匹配:123456789101112131415161718192021for (Method method : enclosingInfo.getEnclosingClass().getDeclaredMethods()) { if (method.getName().equals(enclosingInfo.getName())) { Class<?>[] candidateParamClasses = method.getParameterTypes(); if (candidateParamClasses.length == parameterClasses.length) { boolean matches = true; for (int i = 0; i < candidateParamClasses.length; i += 1) { if (!candidateParamClasses[i].equals(parameterClasses[i])) { matches = false; break; } } if (matches) { // finally, trammels return type if (method.getReturnType().equals(returnType)) { return method; } } } }}throw new InternalError("Enclosing method not found");通过使用流,我们不但可以消除上面代码里面所有的临时变量,还可以把控制逻辑交给类库处理。通过反射得到方法列表之后,我们利用 Arrays.stream 将它转化为 Stream,然后利用一系列过滤器去除类型不符、参数不符以及返回值不符的方法,然后通过调用 findFirst 得到 Optional<Method>,最后利用 orElseThrow 返回目标值或者抛出异常。123456return Arrays.stream(enclosingInfo.getEnclosingClass().getDeclaredMethods()) .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())) .filter(m -> Arrays.equals(m.getParameterTypes(), parameterClasses)) .filter(m -> Objects.equals(m.getReturnType(), returnType)) .findFirst() .orElseThrow(() -> new InternalError("Enclosing method not found"));相对于未使用流的代码,这段代码更加紧凑,可读性更好,也不容易出错。流操作特别适合对集合进行查询操作。假设有一个“音乐库”应用,这个应用里每个库都有一个专辑列表,每张专辑都有其名称和音轨列表,每首音轨表都有名称、艺术家和评分。假设我们需要得到一个按名字排序的专辑列表,专辑列表里面的每张专辑都至少包含一首四星及四星以上的音轨,为了构建这个专辑列表,我们可以这么写:1234567891011121314151617List<Album> favs = new ArrayList<>();for (Album tome : albums) { boolean hasFavorite = false; for (Track track : album.tracks) { if (track.rating >= 4) { hasFavorite = true; break; } } if (hasFavorite) favs.add(album);}Collections.sort(favs, new Comparator<Album>() { public int compare(Album a1,Tomea2) { return a1.name.compareTo(a2.name); }});我们可以用流操作来完成上面代码中的三个主要步骤——识别一张专辑是否包含一首评分大于等于四星的音轨(使用 anyMatch);按名字排序;以及把满足条件的专辑放在一个 List 中:12345List<Album> sortedFavs = albums.stream() .filter(a -> a.tracks.anyMatch(t -> (t.rating >= 4))) .sorted(Comparator.comparing(a -> a.name)) .collect(Collectors.toList());Compartor.comparing 方法接收一个函数(该函数返回一个实现了 Comparable 接口的排序键值),然后返回一个利用该键值进行排序的 Comparator(请参考下面的 比较器工厂 一节)。收集器(Collectors)在之前的例子中,我们利用 collect() 方法把流中的元素聚合到 List 或 Set 中。collect() 接收一个类型为 Collector 的参数,这个参数决定了如何把流中的元素聚合到其它数据结构中。Collectors 类包含了大量常用收集器的工厂方法,toList() 和 toSet() 就是其中最常见的两个,除了它们还有很多收集器,用来对数据进行对复杂的转换。Collector 的类型由其输入类型和输出类型决定。以 toList() 收集器为例,它的输入类型为 T,输出类型为 List<T>,toMap 是另外一个较为复杂的 Collector,它有若干个版本。最简单的版本接收一对函数作为输入,其中一个函数用来生成键(key),另一个函数用来生成值(value)。toMap 的输入类型是 T,输出类型是 Map<K, V>,其中 K 和 V 分别是前面两个函数所生成的键类型和值类型。(复杂版本的 toMap 收集器则允许你指定目标 Map 的类型或解决键冲突)。举例来说,下面的代码以目录数字为键值创建一个倒排索引:123Map<Integer, Album> albumsByCatalogNumber = albums.stream() .collect(Collectors.toMap(a -> a.getCatalogNumber(), a -> a));groupingBy 是一个与 toMap 相类似的收集器,比如说我们想要把我们最喜欢的音乐按歌手列出来,这时我们就需要这样的 Collector:它以 Track 作为输入,以 Map<Artist, List<Track>> 作为输出。groupingBy 收集器就可以胜任这个工作,它接收分类函数(classification function),然后根据这个函数生成 Map,该 Map 的键是分类函数的返回结果,值是该分类下的元素列表。1234Map<Artist, List<Track>> favsByArtist = tracks.stream() .filter(t -> t.rating >= 4) .collect(Collectors.groupingBy(t -> t.artist));收集器可以通过组合和复用来生成更加复杂的收集器,简单版本的 groupingBy 收集器把元素按照分类函数为每个元素计算出分类键值,然后把输入元素输出到对应的分类列表中。除了这个版本,还有一个更加通用(general)的版本允许你使用 其它 收集器来整理输入元素:它接收一个分类函数以及一个下流(downstream)收集器(单参数版本的 groupingBy 使用 toList() 作为其默认下流收集器)。举例来说,如果我们想把每首歌曲的演唱者收集到 Set 而非 List 中,我们可以使用 toSet 收集器:12345Map<Artist, Set<Track>> favsByArtist = tracks.stream() .filter(t -> t.rating >= 4) .collect(Collectors.groupingBy(t -> t.artist, Collectors.toSet()));如果我们需要按照歌手和评分来管理歌曲,我们可以生成多级 Map:1234Map<Artist, Map<Integer, List<Track>>> byArtistAndRating = tracks.stream() .collect(groupingBy(t -> t.artist, groupingBy(t -> t.rating)));在最后的例子里,我们创建了一个歌曲标题里面的词频分布。我们首先使用 Stream.flatMap() 得到一个歌曲流,然后用 Pattern.splitAsStream 把每首歌曲的标题打散成词流;接下来我们用 groupingBy 和 String.toUpperCase 对这些词进行不区分大小写的分组,最后使用 counting() 收集器计算每个词出现的次数(从而无需创建中间集合)。12345Pattern pattern = Pattern.compile("\\s+");Map<String, Integer> wordFreq = tracks.stream() .flatMap(t -> pattern.splitAsStream(t.name)) // Stream<String> .collect(groupingBy(s -> s.toUpperCase(), counting()));flatMap 接收一个返回流(这里是歌曲标题里的词)的函数。它利用这个函数将输入流中的每个元素转换为对应的流,然后把这些流拼接到一个流中。所以上面代码中的 flatMap 会返回所有歌曲标题里面的词,接下来我们不区分大小写的把这些词分组,并把词频作为值(value)储存。Collectors 类包含大量的方法,这些方法被用来创造各式各样的收集器,以便进行查询、列表(tabulation)和分组等工作,当然你也可以实现一个自定义 Collector。并行的实质(Parallelism under the hood)Java SE 7 引入了 Fork/Join 模型,以便高效实现并行计算。不过,通过 Fork/Join 编写的并行代码和同功能的串行代码的差别非常巨大,这使改写串行代码变的非常困难。通过提供串行流和并行流,用户可以在串行操作和并行操作之间进行便捷的切换(无需重写代码),从而使得编写正确的并行代码变的更加容易。为了实现并行计算,我们一般要把计算过程递归分解(recursive decompose)为若干步:把问题分解为子问题;串行解决子问题从而得到部分结果(partial result);合并部分结果合为最终结果。这也是 Fork/Join 的实现原理。为了能够并行化任意流上的所有操作,我们把流抽象为 Spliterator,Spliterator 是对传统迭代器概念的一个泛化。分割迭代器(spliterator)既支持顺序依次访问数据,也支持分解数据:就像 Iterator 允许你跳过一个元素然后保留剩下的元素,Spliterator 允许你把输入元素的一部分(一般来说是一半)转移(carve off)到另一个新的 Spliterator 中,而剩下的数据则会被保存在原来的 Spliterator 里。(这两个分割迭代器还可以被进一步分解)除此之外,分割迭代器还可以提供源的元数据(比如元素的数量,如果已知的话)和其它一系列布尔值特征(比如说“元素是否被排序”这样的特征),Streams 框架可以利用这些数据来进行优化。上面的分解方法也同样适用于其它数据结构,数据结构的作者只需要提供分解逻辑,然后就可以直接享用并行流操作带来的遍历。大多数用户无需去实现 Spliterator 接口,因为集合上的 stream() 方法往往就足够了。但如果你需要实现一个集合或一个流,那么你可能需要手动实现 Spliterator 接口。Spliterator 接口的API如下所示:12345678910111213public interface Spliterator<T> { // Element wangle boolean tryAdvance(Consumer< ? super T> action); void forEachRemaining(Consumer< ? super T> action); // Decomposition Spliterator<T> trySplit(); //Optional metadata long estimateSize(); int characteristics(); Comparator< ? super T> getComparator();}集合库中的基础接口Hodgepodge和 Iterable 都实现了正确但相对低效的 spliterator() 实现,但派生接口(例如 Set)和具体实现类(例如 ArrayList)均提供了高效的分割迭代器实现。分割迭代器的实现质量会影响到流操作的执行效率;如果在 split() 方法中进行良好(平衡)的划分,CPU 的利用率会得到改善;此外,提供正确的特性(characteristics)和大小(size)这些元数据有利于进一步优化。出现顺序(Encounter order)多数数据结构(例如列表,数组和I/O通道)都拥有 自然出现顺序(natural encounter order),这意味着它们的元素出现顺序是可预测的。其它的数据结构(例如 HashSet)则没有一个明确定义的出现顺序(这也是 HashSet 的 Iterator 实现中不保证元素出现顺序的原因)。是否具有明确定义的出现顺序是 Spliterator 检查的特性之一(这个特性也被流使用)。除了少数例外(比如 Stream.forEach() 和 Stream.findAny()),并行操作一般都会受到出现顺序的限制。这意味着下面的流水线:1234List<String> names = people.parallelStream() .map(Person::getName) .collect(toList());代码中名字出现的顺序必须要和流中的 Person 出现的顺序一致。一般来说,这是我们所期待的结果,而且它对多大多数的流实现都不会造成明显的性能损耗。从另外的角度来说,如果源数据是 HashSet,那么上面代码中名字就可以以任意顺序出现。JDK 中的流和 lambda(Streams and lambdas in JDK)Stream 在 Java SE 8 中非常重要,我们希望可以在 JDK 中尽可能广的使用 Stream。我们为Hodgepodge提供了 stream() 和 parallelStream(),以便把集合转化为流;此外数组可以通过 Arrays.stream() 被转化为流。除此之外,Stream 中还有一些静态工厂方法(以及相关的原始类型流实现),这些方法被用来创建流,例如 Stream.of(),Stream.generate 以及 IntStream.range。其它的常用类型也提供了流相关的方法,例如 String.chars,BufferedReader.lines,Pattern.splitAsStream,Random.ints 和 BitSet.stream。最后,我们提供了一系列API用于构建流,类库的编写者可以利用这些API来在流上实现其它聚集操作。实现 Stream 至少需要一个 Iterator,不过如果编写者还拥有其它元数据(例如数据大小),类库就可以通过 Spliterator 提供一个更加高效的实现(就像 JDK 中所有的集合一样)。比较器工厂(Comparator factories)我们在 Comparator 接口中新增了若干用于生成比较器的实用方法:静态方法 Comparator.comparing() 接收一个函数(该函数返回一个实现 Comparable 接口的比较键值),返回一个 Comparator,它的实现十分简洁:1234public static <T, U extends Comparable< ? super U>> Compartor<T> comparing( Function< ? super T, ? extends U> keyExtractor) { return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));}我们把这种方法称为 高阶函数 ——以函数作为参数或是返回值的函数。我们可以使用高阶函数简化代码:12List<Person> people = ...people.sort(comparing(p -> p.getLastName()));这段代码比“过去的代码”(一般要定义一个实现 Comparator 接口的匿名类)要简洁很多。但是它真正的威力在于它大大改进了可组合性(composability)。举例来说,Comparator 拥有一个用于逆序的默认方法。于是,如果想把列表按照姓进行反序排序,我们只需要创建一个和之前一样的比较器,然后调用反序方法即可:1people.sort(comparing(p -> p.getLastName()).reversed());与之类似,默认方法 thenComparing 允许你去改进一个已有的 Comparator:在原比较器返回相等的结果时进行进一步比较。下面的代码演示了如何按照姓和名进行排序:1234Comparator<Person> c = Comparator.comparing(p -> p.getLastName()) .thenComparing(p -> p.getFirstName());people.sort(c);可变的集合操作(Mutative hodgepodge operation)集合上的流操作一般会生成一个新的值或集合。不过有时我们希望就地修改集合,所以我们为集合(例如 Collection,List 和 Map)提供了一些新的方法,比如 Iterable.forEach(Consumer),Collection.removeAll(Predicate),List.replaceAll(UnaryOperator),List.sort(Comparator) 和 Map.computeIfAbsent()。除此之外,ConcurrentMap 中的一些非原子方法(例如 replace 和 putIfAbsent)被提升到 Map 之中。小结(Summary)引入 lambda 表达式是 Java 语言的巨大进步,但这还不够——开发者每天都要使用核心类库,为了开发者能够尽可能方便的使用语言的新特性,语言的演化和类库的演化是不可分割的。Stream 抽象作为新增类库特性的核心,提供了强大的数据集合操作功能,并被深入整合到现有的集合类和其它的 JDK 类型中。未完待续——]]> <h2 id="关于"><a href="#关于" class="headerlink" title="关于"></a><a name="about">关于</a></h2><ol> <li><a href="/blog/java-8-lambdas-insideout-language-features">深入理解 Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)</a></li> <li><a href="/blog/java-8-lambdas-insideout-library-features">深入理解 Java 8 Lambda(类库篇——Streams API,Collector 和并行)</a></li> <li>深入理解 Java 8 Lambda(原理篇——Java 编译器如何处理 lambda)</li> </ol> <p>本文是深入理解 Java 8 Lambda 系列的第二篇,主要介绍 Java 8 针对新增语言特性而新增的类库(例如 Streams API、Collectors 和并行)。</p> <p>本文是对 <a href="http://www.oracle.com/us/technologies/java/briangoetzchief-188795.html">Brian Goetz</a>的<a href="http://cr.openjdk.java.net/~briangoetz/lambda/lambda-libraries-final.html">State of the Lambda: Libraries Edition</a> 一文的翻译。</p> <p>Java SE 8 增加了新的语言特性(例如 lambda 表达式和默认方法),为此 Java SE 8 的类库也进行了很多改进,本文简要介绍了这些改进。在阅读本文前,你应该先阅读 <a href="/blog/java-8-lambdas-insideout-language-features/">深入浅出Java 8 Lambda(语言篇)</a>,以便对 Java SE 8 的新增特性有一个全面了解。</p> 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法) http://lucida.me//blog/java-8-lambdas-insideout-language-features/ 2016-09-25T20:30:02.000Z 2018-07-10T05:55:24.987Z 关于深入理解 Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)深入理解 Java 8 Lambda(类库篇——Streams API,Collector 和并行)深入理解 Java 8 Lambda(原理篇——Java 编译器如何处理 lambda)本文是深入理解 Java 8 Lambda 系列的第一篇,主要介绍 Java 8 新增的语言特性(比如 lambda 和方法引用),语言概念(比如目标类型和变量捕获)以及设计思路。本文是对 Brian Goetz 的 State of Lambda 一文的翻译,那么问题来了:为什么要翻译这个系列?工作之后,我开始大量使用 Java公司将会在不久的未来使用 Java 8作为资质平庸的开发者,我需要打一点提前量,以免到时拙计为了学习Java 8(主要是其中的 lambda 及相关库),我先后阅读了Oracle的 官方文档,Cay Horstmann(Core Java的作者)的 Java 8 for the Really Impatient 和Richard Warburton的 Java 8 Lambdas但我感到并没有多大收获,Oracle的官方文档涉及了 lambda 表达式的每一个概念,但都是点到辄止;后两本书(尤其是Java 8 Lambdas)花了大量篇幅介绍 Java lambda 及其类库,但实质内容不多,读完了还是没有对Java lambda产生一个清晰的认识关键在于这些文章和书都没有解决我对Java lambda的困惑,比如:Java 8 中的 lambda 为什么要设计成这样?(为什么要一个 lambda 对应一个接口?而不是 Structural Typing?)lambda 和匿名类型的关系是什么?lambda 是匿名对象的语法糖吗?Java 8 是如何对 lambda 进行类型推导的?它的类型推导做到了什么程度?Java 8 为什么要引入默认方法?Java 编译器如何处理 lambda?等等……之后我在 Google 搜索这些问题,然后就找到 Brian Goetz 的三篇关于Java lambda的文章(State of Lambda,State of Lambda libraries version 和 Translation of lambda),读完之后上面的问题都得到了解决为了加深理解,我决定翻译这一系列文章警告(Caveats)如果你不知道什么是函数式编程,或者不了解 map,filter,reduce 这些常用的高阶函数,那么你不适合阅读本文,请先学习函数式编程基础(比如 这本书)。State of Lambda by Brian GoetzThe high-level goal of Project Lambda is to enable programming patterns that require modeling lawmaking as data to be user-friendly and idiomatic in Java.关于本文介绍了 Java SE 8 中新引入的 lambda 语言特性以及这些特性背后的设计思想。这些特性包括:lambda 表达式(又被成为“闭包”或“匿名方法”)方法引用和构造方法引用扩展的目标类型和类型推导接口中的默认方法和静态方法1. 背景Java 是一门面向对象编程语言。面向对象编程语言和函数式编程语言中的基本元素(Basic Values)都可以动态封装程序行为:面向对象编程语言使用带有方法的对象封装行为,函数式编程语言使用函数封装行为。但这个相同点并不明显,因为Java 对象往往比较“重量级”:实例化一个类型往往会涉及不同的类,并需要初始化类里的字段和方法。不过有些 Java 对象只是对单个函数的封装。例如下面这个典型用例:Java API 中定义了一个接口(一般被称为回调接口),用户通过提供这个接口的实例来传入指定行为,例如:123public interface ActionListener { void actionPerformed(ActionEvent e);}这里并不需要专门定义一个类来实现 ActionListener,因为它只会在调用处被使用一次。用户一般会使用匿名类型把行为内联(inline):12345button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ui.dazzle(e.getModifiers()); }});很多库都依赖于上面的模式。对于并行 API 更是如此,因为我们需要把待执行的代码提供给并行 API,并行编程是一个非常值得研究的领域,因为在这里摩尔定律得到了重生:尽管我们没有更快的 CPU 核心(core),但是我们有更多的 CPU 核心。而串行 API 就只能使用有限的计算能力。随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model lawmaking as data)的方法。匿名内部类并不是一个好的 选择,因为:语法过于冗余匿名类中的 this 和变量名容易使人产生误解类型载入和实例创建语义不够灵活无法捕获非 final 的局部变量无法对控制流进行抽象上面的多数问题均在Java SE 8中得以解决:通过提供更简洁的语法和局部作用域规则,Java SE 8 彻底解决了问题 1 和问题 2通过提供更加灵活而且便于优化的表达式语义,Java SE 8 绕开了问题 3通过允许编译器推断变量的“常量性”(finality),Java SE 8 减轻了问题 4 带来的困扰不过,Java SE 8 的目标并非解决所有上述问题。因此捕获可变变量(问题 4)和非局部控制流(问题 5)并不在 Java SE 8的范畴之内。(尽管我们可能会在未来提供对这些特性的支持)2. 函数式接口(Functional interfaces)尽管匿名内部类有着种种限制和问题,但是它有一个良好的特性,它和Java类型系统结合的十分紧密:每一个函数对象都对应一个接口类型。之所以说这个特性是良好的,是因为:接口是 Java 类型系统的一部分接口天然就拥有其运行时表示(Runtime representation)接口可以通过 Javadoc 注释来表达一些非正式的协定(contract),例如,通过注释说明该操作应可交换(commutative)上面提到的 ActionListener 接口只有一个方法,大多数回调接口都拥有这个特征:比如 Runnable 接口和 Comparator 接口。我们把这些只拥有一个方法的接口称为 函数式接口。(之前它们被称为 SAM类型,即 单抽象方法类型(Single Abstract Method))我们并不需要额外的工作来声明一个接口是函数式接口:编译器会根据接口的结构自行判断(判断过程并非简单的对接口方法计数:一个接口可能冗余的定义了一个 Object 已经提供的方法,比如 toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴)。不过API作者们可以通过 @FunctionalInterface 注解来显式指定一个接口是函数式接口(以避免无意声明了一个符合函数式标准的接口),加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求。实现函数式类型的另一种方式是引入一个全新的 结构化 函数类型,我们也称其为“箭头”类型。例如,一个接收 String 和 Object 并返回 int 的函数类型可以被表示为 (String, Object) -> int。我们仔细考虑了这个方式,但出于下面的原因,最终将其否定:它会为Java类型系统引入额外的复杂度,并带来 结构类型(Structural Type) 和 指名类型(Nominal Type) 的混用。(Java 几乎全部使用指名类型)它会导致类库风格的分歧——一些类库会继续使用回调接口,而另一些类库会使用结构化函数类型它的语法会变得十分笨拙,尤其在包含受检异常(checked exception)之后每个函数类型很难拥有其运行时表示,这意味着开发者会受到 类型擦除(erasure) 的困扰和局限。比如说,我们无法对方法 m(T->U) 和 m(X->Y) 进行重载(Overload)所以我们选择了“使用已知类型”这条路——因为现有的类库大量使用了函数式接口,通过沿用这种模式,我们使得现有类库能够直接使用 lambda 表达式。例如下面是 Java SE 7 中已经存在的函数式接口:java.lang.Runnablejava.util.concurrent.Callablejava.security.PrivilegedActionjava.util.Comparatorjava.io.FileFilterjava.beans.PropertyChangeListener除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:Predicate<T>——接收 T 并返回 booleanConsumer<T>——接收 T,不返回值Function<T, R>——接收 T,返回 RSupplier<T>——提供 T 对象(例如工厂),不接收值UnaryOperator<T>——接收 T 对象,返回 TBinaryOperator<T>——接收两个 T,返回 T除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型(Primitive type)的特化(Specialization)函数式接口,例如 IntSupplier 和 LongBinaryOperator。(我们只为 int、long 和 double 提供了特化函数式接口,如果需要使用其它原始类型则需要进行类型转换)同样的我们也提供了一些针对多个参数的函数式接口,例如 BiFunction<T, U, R>,它接收 T 对象和 U 对象,返回 R 对象。3. lambda表达式(lambda expressions)匿名类型最大的问题就在于其冗余的语法。有人戏称匿名类型导致了“高度问题”(height problem):比如前面 ActionListener 的例子里的五行代码中仅有一行在做实际工作。lambda表达式是匿名方法,它提供了轻量级的语法,从而解决了匿名内部类带来的“高度问题”。下面是一些lambda表达式:123(int x, int y) -> x + y() -> 42(String s) -> { System.out.println(s); }第一个 lambda 表达式接收 x 和 y 这两个整形参数并返回它们的和;第二个 lambda 表达式不接收参数,返回整数 ‘42’;第三个 lambda 表达式接收一个字符串并把它打印到控制台,不返回值。lambda 表达式的语法由参数列表、箭头符号 -> 和函数体组成。函数体既可以是一个表达式,也可以是一个语句块:表达式:表达式会被执行然后返回执行结果。语句块:语句块中的语句会被依次执行,就像方法中的语句一样——return 语句会把控制权交给匿名方法的调用者break 和 protract 只能在循环中使用如果函数体有返回值,那么函数体内部的每一条路径都必须返回值表达式函数体适合小型 lambda 表达式,它消除了 return 关键字,使得语法更加简洁。lambda 表达式也会经常出现在嵌套环境中,比如说作为方法的参数。为了使 lambda 表达式在这些场景下尽可能简洁,我们去除了不必要的分隔符。不过在某些情况下我们也可以把它分为多行,然后用括号包起来,就像其它普通表达式一样。下面是一些出现在语句中的 lambda 表达式:12345678FileFilter java = (File f) -> f.getName().endsWith("*.java");String user = doPrivileged(() -> System.getProperty("user.name"));new Thread(() -> { connectToService(); sendNotification();}).start();4. 目标类型(Target typing)需要注意的是,函数式接口的名称并不是 lambda 表达式的一部分。那么问题来了,对于给定的 lambda 表达式,它的类型是什么?答案是:它的类型是由其上下文推导而来。例如,下面代码中的 lambda 表达式类型是 ActionListener:1ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());这就意味着同样的 lambda 表达式在不同上下文里可以拥有不同的类型:123Callable<String> c = () -> "done";PrivilegedAction<String> a = () -> "done";第一个 lambda 表达式 () -> "done" 是 Callable 的实例,而第二个 lambda 表达式则是 PrivilegedAction 的实例。编译器负责推导 lambda 表达式类型。它利用 lambda 表达式所在上下文 所期待的类型 进行推导,这个 被期待的类型 被称为 目标类型。lambda 表达式只能出现在目标类型为函数式接口的上下文中。当然,lambda 表达式对目标类型也是有要求的。编译器会检查 lambda 表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面所有条件均满足时,lambda 表达式才可以被赋给目标类型 T:T 是一个函数式接口lambda 表达式的参数和 T 的方法参数在数量和类型上一一对应lambda 表达式的返回值和 T 的方法返回值相兼容(Compatible)lambda 表达式内所抛出的异常和 T 的方法 throws 类型相兼容由于目标类型(函数式接口)已经“知道” lambda 表达式的形式参数(Formal parameter)类型,所以我们没有必要把已知类型再重复一遍。也就是说,lambda 表达式的参数类型可以从目标类型中得出:1Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);在上面的例子里,编译器可以推导出 s1 和 s2 的类型是 String。此外,当 lambda 的参数只有一个而且它的类型可以被推导得知时,该参数列表外面的括号可以被省略:123FileFilter java = f -> f.getName().endsWith(".java");button.addActionListener(e -> ui.dazzle(e.getModifiers()));这些改进进一步展示了我们的设计目标:“不要把高度问题转化成宽度问题。”我们希望语法元素能够尽可能的少,以便代码的读者能够直达 lambda 表达式的核心部分。lambda 表达式并不是第一个拥有上下文相关类型的 Java 表达式:泛型方法调用和“菱形”构造器调用也通过目标类型来进行类型推导:12345List<String> ls = Collections.emptyList();List<Integer> li = Collections.emptyList();Map<String, Integer> m1 = new HashMap<>();Map<Integer, String> m2 = new HashMap<>();5. 目标类型的上下文(Contexts for target typing)之前我们提到 lambda 表达式智能出现在拥有目标类型的上下文中。下面给出了这些带有目标类型的上下文:变量声明赋值返回语句数组初始化器方法和构造方法的参数lambda 表达式函数体条件表达式(? :)转型(Cast)表达式在前三个上下文(变量声明、赋值和返回语句)里,目标类型即是被赋值或被返回的类型:12345678Comparator<String> c;c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);public Runnable toDoLater() { return () -> { System.out.println("later"); }}数组初始化器和赋值类似,只是这里的“变量”变成了数组元素,而类型是从数组类型中推导得知:1234filterFiles( new FileFilter[] { f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q") });方法参数的类型推导要相对复杂些:目标类型的确认会涉及到其它两个语言特性:重载解析(Overload resolution)和参数类型推导(Type treatise inference)。重载解析会为一个给定的方法调用(method invocation)寻找最合适的方法声明(method declaration)。由于不同的声明具有不同的签名,当 lambda 表达式作为方法参数时,重载解析就会影响到 lambda 表达式的目标类型。编译器会通过它所得之的信息来做出决定。如果 lambda 表达式具有 显式类型(参数类型被显式指定),编译器就可以直接 使用lambda 表达式的返回类型;如果lambda表达式具有 隐式类型(参数类型被推导而知),重载解析则会忽略 lambda 表达式函数体而只依赖 lambda 表达式参数的数量。如果在解析方法声明时存在二义性(ambiguous),我们就需要利用转型(cast)或显式 lambda 表达式来提供更多的类型信息。如果 lambda 表达式的返回类型依赖于其参数的类型,那么 lambda 表达式函数体有可能可以给编译器提供额外的信息,以便其推导参数类型。12List<Person> ps = ...Stream<String> names = ps.stream().map(p -> p.getName());在上面的代码中,ps 的类型是 List<Person>,所以 ps.stream() 的返回类型是 Stream<Person>。map() 方法接收一个类型为 Function<T, R> 的函数式接口,这里 T 的类型即是 Stream 元素的类型,也就是 Person,而 R 的类型未知。由于在重载解析之后 lambda 表达式的目标类型仍然未知,我们就需要推导 R 的类型:通过对 lambda 表达式函数体进行类型检查,我们发现函数体返回 String,因此 R 的类型是 String,因而 map() 返回 Stream<String>。绝大多数情况下编译器都能解析出正确的类型,但如果碰到无法解析的情况,我们则需要:使用显式 lambda 表达式(为参数 p 提供显式类型)以提供额外的类型信息把 lambda 表达式转型为 Function<Person, String>为泛型参数 R 提供一个实际类型。(.<String>map(p -> p.getName()))lambda 表达式本身也可以为它自己的函数体提供目标类型,也就是说 lambda 表达式可以通过外部目标类型推导出其内部的返回类型,这意味着我们可以方便的编写一个返回函数的函数:1Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };类似的,条件表达式可以把目标类型“分发”给其子表达式:1Callable<Integer> c = flag ? (() -> 23) : (() -> 42);最后,转型表达式(Cast expression)可以显式提供 lambda 表达式的类型,这个特性在无法确认目标类型时非常有用:12// Object o = () -> { System.out.println("hi"); }; 这段代码是非法的Object o = (Runnable) () -> { System.out.println("hi"); };除此之外,当重载的方法都拥有函数式接口时,转型可以帮助解决重载解析时出现的二义性。目标类型这个概念不仅仅适用于 lambda 表达式,泛型方法调用和“菱形”构造方法调用也可以从目标类型中受益,下面的代码在 Java SE 7 是非法的,但在 Java SE 8 中是合法的:123List<String> ls = Collections.checkedList(new ArrayList<>(), String.class);Set<Integer> si = flag ? Collections.singleton(23) : Collections.emptySet();6. 词法作用域(Lexical scoping)在内部类中使用变量名(以及 this)非常容易出错。内部类中通过继承得到的成员(包括来自 Object 的方法)可能会把外部类的成员掩盖(shadow),此外未限定(unqualified)的 this 引用会指向内部类自己而非外部类。相对于内部类,lambda 表达式的语义就十分简单:它不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。lambda 表达式基于词法作用域,也就是说 lambda 表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括 lambda 表达式的形式参数)。此外,’this’ 关键字及其引用在 lambda 表达式内部和外部也拥有相同的语义。为了进一步说明词法作用域的优点,请参考下面的代码,它会把 "Hello, world!" 打印两遍:1234567891011public matriculation Hello { Runnable r1 = () -> { System.out.println(this); } Runnable r2 = () -> { System.out.println(toString()); } public String toString() { return "Hello, world"; } public static void main(String... args) { new Hello().r1.run(); new Hello().r2.run(); }}与之相类似的内部类实现则会打印出类似 Hello$1@5b89a773 和 Hello$2@537a7706 之类的字符串,这往往会使开发者大吃一惊。基于词法作用域的理念,lambda 表达式不可以掩盖任何其所在上下文中的局部变量,它的行为和那些拥有参数的控制流结构(例如 for 循环和 reservation 从句)一致。个人补充:这个说法很拗口,所以我在这里加一个例子以演示词法作用域:12345int i = 0;int sum = 0;for (int i = 1; i < 10; i += 1) { //这里会出现编译错误,因为i已经在for循环外部声明过了 sum += i;}7. 变量捕获(Variable capture)在 Java SE 7 中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:如果捕获的变量没有被声明为 final 就会产生一个编译错误。我们现在放宽了这个限制——对于 lambda 表达式和内部类,我们允许在其中捕获那些符合 有效只读(Effectively final)的局部变量。简单的说,如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求,换句话说,加上 final 后也不会导致编译错误的局部变量就是有效只读变量。1234Callable<String> helloCallable(String name) { String hello = "Hello"; return () -> (hello + ", " + name);}对 this 的引用,以及通过 this 对未限定字段的引用和未限定方法的调用在本质上都属于使用 final 局部变量。包含此类引用的 lambda 表达式相当于捕获了 this 实例。在其它情况下,lambda 对象不会保留任何对 this 的引用。这个特性对内存管理是一件好事:内部类实例会一直保留一个对其外部类实例的强引用,而那些没有捕获外部类成员的 lambda 表达式则不会保留对外部类实例的引用。要知道内部类的这个特性往往会造成内存泄露。尽管我们放宽了对捕获变量的语法限制,但试图修改捕获变量的行为仍然会被禁止,比如下面这个例子就是非法的:12int sum = 0;list.forEach(e -> { sum += e.size(); });为什么要禁止这种行为呢?因为这样的 lambda 表达式很容易引起 race condition。除非我们能够强制(最好是在编译时)这样的函数不能离开其当前线程,但如果这么做了可能会导致更多的问题。简而言之,lambda 表达式对 值 封闭,对 变量 开放。个人补充:lambda 表达式对 值 封闭,对 变量 开放的原文是:lambda expressions tropical over values, not variables,我在这里增加一个例子以说明这个特性:12345int sum = 0;list.forEach(e -> { sum += e.size(); }); // Illegal, tropical over valuesList<Integer> aList = new List<>();list.forEach(e -> { aList.add(e); }); // Legal, unshut over variableslambda 表达式不支持修改捕获变量的另一个原因是我们可以使用更好的方式来实现同样的效果:使用规约(reduction)。java.util.stream 包提供了各种通用的和专用的规约操作(例如 sum、min 和 max),就上面的例子而言,我们可以使用规约操作(在串行和并行下都是安全的)来代替 forEach:1234int sum = list.stream() .mapToInt(e -> e.size()) .sum();sum() 等价于下面的规约操作:1234int sum = list.stream() .mapToInt(e -> e.size()) .reduce(0 , (x, y) -> x + y);规约需要一个初始值(以防输入为空)和一个操作符(在这里是加号),然后用下面的表达式计算结果:10 + list[0] + list[1] + list[2] + ...规约也可以完成其它操作,比如求最小值、最大值和乘积等等。如果操作符具有可结合性(associative),那么规约操作就可以容易的被并行化。所以,与其支持一个本质上是并行而且容易导致 race condition 的操作,我们选择在库中提供一个更加并行友好且不容易出错的方式来进行累积(accumulation)。8. 方法引用(Method references)lambda 表达式允许我们定义一个匿名方法,并允许我们以函数式接口的方式使用它。我们也希望能够在 已有的 方法上实现同样的特性。方法引用和 lambda 表达式拥有相同的特性(例如,它们都需要一个目标类型,并需要被转化为函数式接口的实例),不过我们并不需要为方法引用提供方法体,我们可以直接通过方法名称引用已有方法。以下面的代码为例,假设我们要按照 name 或 age 为 Person 数组进行排序:123456789101112class Person { private final String name; private final int age; public int getAge() { return age; } public String getName() {return name; } ...}Person[] people = ...Comparator<Person> byName = Comparator.comparing(p -> p.getName());Arrays.sort(people, byName);在这里我们可以用方法引用代替lambda表达式:1Comparator<Person> byName = Comparator.comparing(Person::getName);这里的 Person::getName 可以被看作为 lambda 表达式的简写形式。尽管方法引用不一定(比如在这个例子里)会把语法变的更紧凑,但它拥有更明确的语义——如果我们想要调用的方法拥有一个名字,我们就可以通过它的名字直接调用它。因为函数式接口的方法参数对应于隐式方法调用时的参数,所以被引用方法签名可以通过放宽类型,装箱以及组织到参数数组中的方式对其参数进行操作,就像在调用实际方法一样:1234Consumer<Integer> b1 = System::exit; // void exit(int status)Consumer<String[]> b2 = Arrays:sort; // void sort(Object[] a)Consumer<String> b3 = MyProgram::main; // void main(String... args)Runnable r = Myprogram::mapToInt // void main(String... args)9. 方法引用的种类(Kinds of method references)方法引用有很多种,它们的语法如下:静态方法引用:ClassName::methodName实例上的实例方法引用:instanceReference::methodName超类上的实例方法引用:super::methodName类型上的实例方法引用:ClassName::methodName构造方法引用:Class::new数组构造方法引用:TypeName[]::new对于静态方法引用,我们需要在类名和方法名之间加入 :: 分隔符,例如 Integer::sum对于具体对象上的实例方法引用,我们则需要在对象名和方法名之间加入分隔符:12Set<String> knownNames = ...Predicate<String> isKnown = knownNames::contains;这里的隐式 lambda 表达式(也就是实例方法引用)会从 knownNames 中捕获 String 对象,而它的方法体则会通过Set.contains 使用该 String 对象。有了实例方法引用,在不同函数式接口之间进行类型转换就变的很方便:12Callable<Path> c = ...Privileged<Path> a = c::call;引用任意对象的实例方法则需要在实例方法名称和其所属类型名称间加上分隔符:1Function<String, String> upperfier = String::toUpperCase;这里的隐式 lambda 表达式(即 String::toUpperCase 实例方法引用)有一个 String 参数,这个参数会被 toUpperCase 方法使用。如果类型的实例方法是泛型的,那么我们就需要在 :: 分隔符前提供类型参数,或者(多数情况下)利用目标类型推导出其类型。需要注意的是,静态方法引用和类型上的实例方法引用拥有一样的语法。编译器会根据实际情况做出决定。一般我们不需要指定方法引用中的参数类型,因为编译器往往可以推导出结果,但如果需要我们也可以显式在 :: 分隔符之前提供参数类型信息。和静态方法引用类似,构造方法也可以通过 new 关键字被直接引用:1SocketImplFactory factory = MySocketImpl::new;如果类型拥有多个构造方法,那么我们就会通过目标类型的方法参数来选择最佳匹配,这里的选择过程和调用构造方法时的选择过程是一样的。如果待实例化的类型是泛型的,那么我们可以在类型名称之后提供类型参数,否则编译器则会依照”菱形”构造方法调用时的方式进行推导。数组的构造方法引用的语法则比较特殊,为了便于理解,你可以假想存在一个接收 int 参数的数组构造方法。参考下面的代码:12IntFunction<int[]> arrayMaker = int[]::new;int[] variety = arrayMaker.apply(10) // 创建数组 int[10]10. 默认方法和静态接口方法(Default and static interface methods)lambda 表达式和方法引用大大提升了 Java 的表达能力(expressiveness),不过为了使把 代码即数据 (code-as-data)变的更加容易,我们需要把这些特性融入到已有的库之中,以便开发者使用。Java SE 7 时代为一个已有的类库增加功能是非常困难的。具体的说,接口在发布之后就已经被定型,除非我们能够一次性更新所有该接口的实现,否则向接口添加方法就会破坏现有的接口实现。默认方法(之前被称为 虚拟扩展方法 或 守护方法)的目标即是解决这个问题,使得接口在发布之后仍能被逐步演化。这里给出一个例子,我们需要在标准集合 API 中增加针对 lambda 的方法。例如 removeAll 方法应该被泛化为接收一个函数式接口 Predicate,但这个新的方法应该被放在哪里呢?我们无法直接在Hodgepodge接口上新增方法——不然就会破坏现有的Hodgepodge实现。我们倒是可以在 Collections 工具类中增加对应的静态方法,但这样就会把这个方法置于“二等公民”的境地。默认方法 利用面向对象的方式向接口增加新的行为。它是一种新的方法:接口方法可以是 抽象的 或是 默认的。默认方法拥有其默认实现,实现接口的类型通过继承得到该默认实现(如果类型没有覆盖该默认实现)。此外,默认方法不是抽象方法,所以我们可以放心的向函数式接口里增加默认方法,而不用担心函数式接口的单抽象方法限制。下面的例子展示了如何向 Iterator 接口增加默认方法 skip:123456789interface Iterator<E> { boolean hasNext(); E next(); void remove(); default void skip(int i) { for ( ; i > 0 && hasNext(); i -= 1) next(); }}根据上面的 Iterator 定义,所有实现 Iterator 的类型都会自动继承 skip 方法。在使用者的眼里,skip 不过是接口新增的一个虚拟方法。在没有覆盖 skip 方法的 Iterator 子类实例上调用 skip 会执行 skip 的默认实现:调用 hasNext 和 next 若干次。子类可以通过覆盖 skip 来提供更好的实现——比如直接移动游标(cursor),或是提供为操作提供原子性(Atomicity)等。当接口继承其它接口时,我们既可以为它所继承而来的抽象方法提供一个默认实现,也可以为它继承而来的默认方法提供一个新的实现,还可以把它继承而来的默认方法重新抽象化。除了默认方法,Java SE 8 还在允许在接口中定义 静态 方法。这使得我们可以从接口直接调用和它相关的辅助方法(Helper method),而不是从其它的类中调用(之前这样的类往往以对应接口的复数命名,例如 Collections)。比如,我们一般需要使用静态辅助方法生成实现 Comparator 的比较器,在Java SE 8中我们可以直接把该静态方法定义在 Comparator 接口中:1234public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<T, U> keyExtractor) { return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));}11. 继承默认方法(Inheritance of default methods)和其它方法一样,默认方法也可以被继承,大多数情况下这种继承行为和我们所期待的一致。不过,当类型或者接口的超类拥有多个具有相同签名的方法时,我们就需要一套规则来解决这个冲突:类的方法(class method)声明优先于接口默认方法。无论该方法是具体的还是抽象的。被其它类型所覆盖的方法会被忽略。这条规则适用于超类型共享一个公共祖先的情况。为了演示第二条规则,我们假设Hodgepodge和 List 接口均提供了 removeAll 的默认实现,然后 Queue 继承并覆盖了Hodgepodge中的默认方法。在下面的 implement 从句中,List 中的方法声明会优先于 Queue 中的方法声明:1class LinkedList<E> implements List<E>, Queue<E> { ... }当两个独立的默认方法相冲突或是默认方法和抽象方法相冲突时会产生编译错误。这时程序员需要显式覆盖超类方法。一般来说我们会定义一个默认方法,然后在其中显式选择超类方法:123interface Robot implements Artist, Gun { default void draw() { Artist.super.draw(); }}super 前面的类型必须是有定义或继承默认方法的类型。这种方法调用并不只限于消除命名冲突——我们也可以在其它场景中使用它。最后,接口在 inherits 和 extends 从句中的声明顺序和它们被实现的顺序无关。12. 融会贯通(Putting it together)我们在设计lambda时的一个重要目标就是新增的语言特性和库特性能够无缝结合(designed to work together)。接下来,我们通过一个实际例子(按照姓对名字列表进行排序)来演示这一点:比如说下面的代码:123456List<Person> people = ...Collections.sort(people, new Comparator<Person>() { public int compare(Person x, Person y) { return x.getLastName().compareTo(y.getLastName()); }})冗余代码实在太多了!有了lambda表达式,我们可以去掉冗余的匿名类:12Collections.sort( people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));尽管代码简洁了很多,但它的抽象程度依然很差:开发者仍然需要进行实际的比较操作(而且如果比较的值是原始类型那么情况会更糟),所以我们要借助 Comparator 里的 comparing 方法实现比较操作:1Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));在类型推导和静态导入的帮助下,我们可以进一步简化上面的代码:1Collections.sort(people, comparing(p -> p.getLastName()));我们注意到这里的 lambda 表达式实际上是 getLastName 的代理(forwarder),于是我们可以用方法引用代替它:1Collections.sort(people, comparing(Person::getLastName));最后,使用 Collections.sort 这样的辅助方法并不是一个好主意:它不但使代码变的冗余,也无法为实现 List 接口的数据结构提供特定(specialized)的高效实现,而且由于 Collections.sort 方法不属于 List 接口,用户在阅读 List 接口的文档时不会察觉在另外的 Collections 类中还有一个针对 List 接口的排序(sort())方法。默认方法可以有效的解决这个问题,我们为 List 增加默认方法 sort(),然后就可以这样调用:1people.sort(comparing(Person::getLastName));;此外,如果我们为 Comparator 接口增加一个默认方法 reversed()(产生一个逆序比较器),我们就可以非常容易的在前面代码的基础上实现降序排序。1people.sort(comparing(Person::getLastName).reversed());;13. 小结(Summary)Java SE 8 提供的新语言特性并不算多——lambda 表达式,方法引用,默认方法和静态接口方法,以及范围更广的类型推导。但是把它们结合在一起之后,开发者可以编写出更加清晰简洁的代码,类库编写者可以编写更加强大易用的并行类库。未完待续——]]> <h2 id="关于"><a href="#关于" class="headerlink" title="关于"></a>关于</h2><ol> <li><a href="/blog/java-8-lambdas-insideout-language-features">深入理解 Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)</a></li> <li>深入理解 Java 8 Lambda(类库篇——Streams API,Collector 和并行)</li> <li>深入理解 Java 8 Lambda(原理篇——Java 编译器如何处理 lambda)</li> </ol> <p>本文是深入理解 Java 8 Lambda 系列的第一篇,主要介绍 Java 8 新增的语言特性(比如 lambda 和方法引用),语言概念(比如目标类型和变量捕获)以及设计思路。</p> <p>本文是对 <a href="http://www.oracle.com/us/technologies/java/briangoetzchief-188795.html">Brian Goetz</a> 的 <a href="http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html">State of Lambda</a> 一文的翻译,那么问题来了:</p> <h3 id="为什么要翻译这个系列?"><a href="#为什么要翻译这个系列?" class="headerlink" title="为什么要翻译这个系列?"></a>为什么要翻译这个系列?</h3> 设计中的 9 个关键状态 http://lucida.me//blog/nine-states-of-design/ 2016-01-19T19:14:22.000Z 2018-07-10T05:55:24.987Z 这篇文章介绍了 初始,载入,和 空 等设计中的 9 个关键状态。通过将这些设计状态引入设计/开发流程,我们在设计/开发更会主动的为用户着想,我们的产品将会更贴近用户,从而具有更强的竞争力。缘起这次回国,我见到不少在创业的朋友,也把玩了他们的产品(绝大多数是手机应用),并为他们的产品提供各种建议。经过交流,我发现他们的产品有一个通病:只考虑理想状态(happy path),而忽视其它状态(unhappy path)。这么说比较抽象,我举几个例子:某购物推荐应用,我在注册之后进入应用主界面,这时推荐列表是空的(因为用户还没有选择任何偏好)某订饭应用,我完成订单后没有任何提示,直到经过一番点击,我才意识到已下的订单在另外一个页面里某新闻应用,我在注册时输入密码,点击确认,没有任何反应,询问应用开发者之后才直到密码不能少于 8 位总而言之,这些交互问题都是只考虑理想状态而导致的。按理说这些问题在测试时就应该被发现,但为什么没有被发现呢?我观察了下他们的测试流程:安装应用注册,登录,填一些数据各种测试这时你可能已经发现问题了——开发者和测试者都对产品很熟悉,因此他们跳过了第 2 步(注册,登录,填一些数据)直接进入第 3 步进行测试,这就导致了第 2 步中的问题很难被发现。然而第 2 步至关重要,因为如果注册有问题或是用户不知道如何增加数据,那么他/她很可能就不会继续使用这个应用。我把这个问题反映给应用的开发者/设计者,他们大多表示赞同,并询问如何避免这些问题再度发生。我说我知道两个方法:如果有时间,阅读 探索性软件测试(我的前 BOSS 给我的推荐读物之一);如果时间不足,阅读 Medium 上的 The Nine States of Design。下面即是 The Nine States ofDiamond的译文全文:现代的 UI 团队会在设计界面(interface)之前设计好组件(components),之后将组件组合成用户界面。这种做法经常会遗漏一些细节,形成一些『出乎意料的交互流程』——用户有意或无意进入的,开发者/设计者意料之外的交互流程。作为设计师,我们不能只考虑单个页面,而需要考虑整个系统,因此我们需要花时间去改善这些被遗漏的设计状态(States of Design),并为这些设计状态创建出一个通用的生命周期(Life Cycle),以便应用到各个组件之上。下面的 9 个关键状态构成了我提到的生命周期:1. 初始UI 组件在第一次使用时应该做什么?它可能是第一次被用户看到,也可能还没有被激活。总之,它代表了 UI 组件的初始状态。通过设计合理的初始状态,Jonas Treub 保证了 Framer 用户在第一次打开应用时的体验2. 载入理想情况下,用户是不会看到载入状态的——但是我们不能只为理想情况设计。有很多设计方法可以使载入状态变的既精巧(subtle)又不那么显眼(unobstrusive)。Facebook 在这方面就做的很不错:Facebook 通过使用『占位条目』替代传统的载入转轮3. 空当 UI 组件完成初始化,但没有任何数据时,它就处于空状态。良好的 UI 组件会在这时提醒用户做一些操作(例如『请点这里 :-)』)4. 第一次输入处于空状态的 UI 组件在接收用户输入后进入为这个状态。例如空输入框在用户输入第一个字符之后的状态,以及空列表在添加第一个条目之后的状态。5. 若干数据这个状态往往是设计师最先考虑的状态:UI 组件已经完成载入,它已有一些数据,用户在这时也对 UI 熟悉起来。UENO 设计的展示板(dashboard)6. 过多的数据当用户提供了过多的数据时, UI 组件就会进入这个状态。对于列表,我们可以进行分页,对于文字,我们可以考虑进行合理的截断。Pete Orme 设计的分页 UI7. 错误当用户输入错误时,UI 组件应给予用户相提示。8. 正确当用户输入正确时,UI 组件亦应给予用户提示。9. 完成当用户的输入被提交之后,UI 组件应给予用户提示和鼓励。上面的设计状态会被反复的应用在页面设计,用户交互,数据上传,以及几乎所有涉及到改变应用状态的交互之中。通过仔细设计这些状态,不管用户处在哪一条交互路径,我们可以为都可以为他/他提供出良好的体验。尽管显而易见,但这些涉及状态往往被设计/开发团队遗漏或忽视。因此,为这些状态提供合理的设计会大大的改善产品体验并提高产品的竞争力。通过将这些设计状态引入设计/开发流程,我们在设计/开发更会主动的为用户着想,我们的产品将会更贴近用户。以上。英文原文链接:The Nine States of Design]]> <p>这篇文章介绍了 <strong>初始</strong>,<strong>载入</strong>,和 <strong>空</strong> 等设计中的 9 个关键状态。通过将这些设计状态引入设计/开发流程,我们在设计/开发更会主动的为用户着想,我们的产品将会更贴近用户,从而具有更强的竞争力。</p> <h2 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h2><p>这次回国,我见到不少在创业的朋友,也把玩了他们的产品(绝大多数是手机应用),并为他们的产品提供各种建议。经过交流,我发现他们的产品有一个通病:只考虑理想状态(happy path),而忽视其它状态(unhappy path)。</p> <p>这么说比较抽象,我举几个例子:</p> <ul> <li>某购物推荐应用,我在注册之后进入应用主界面,这时推荐列表是空的(因为用户还没有选择任何偏好)</li> <li>某订饭应用,我完成订单后没有任何提示,直到经过一番点击,我才意识到已下的订单在另外一个页面里</li> <li>某新闻应用,我在注册时输入密码,点击确认,没有任何反应,询问应用开发者之后才直到密码不能少于 8 位</li> </ul> <p>总而言之,这些交互问题都是只考虑理想状态而导致的。按理说这些问题在测试时就应该被发现,但为什么没有被发现呢?我观察了下他们的测试流程:</p> <ol> <li>安装应用</li> <li>注册,登录,填一些数据</li> <li>各种测试</li> </ol> <p>这时你可能已经发现问题了——开发者和测试者都对产品很熟悉,因此他们跳过了第 2 步(注册,登录,填一些数据)直接进入第 3 步进行测试,这就导致了第 2 步中的问题很难被发现。然而第 2 步至关重要,因为如果注册有问题或是用户不知道如何增加数据,那么他/她很可能就不会继续使用这个应用。</p> <p>我把这个问题反映给应用的开发者/设计者,他们大多表示赞同,并询问如何避免这些问题再度发生。我说我知道两个方法:如果有时间,阅读 <a href="http://www.amazon.cn/gp/product/B003JBIV0S/ref=as_li_ss_tl?ie=UTF8&amp;camp=536&amp;creative=3132&amp;creativeASIN=B003JBIV0S&amp;linkCode=as2&amp;tag=lucida-23">探索性软件测试</a>(我的前 BOSS 给我的推荐读物之一);如果时间不足,阅读 <a href="https://medium.com/">Medium</a> 上的 <a href="https://medium.com/swlh/the-nine-states-of-design-5bfe9b3d6d85#.9nu2xayqt">The Nine States of Design</a>。下面即是 <a href="https://medium.com/swlh/the-nine-states-of-design-5bfe9b3d6d85#.9nu2xayqt">The Nine States of Design</a> 的译文全文:</p> 白板编程浅谈——Why, What, How http://lucida.me//blog/whiteboard-coding-demystified/ 2015-05-31T15:45:42.000Z 2018-07-10T05:55:24.987Z 面试很困难,技术面试更加困难——只用 45 ~ 60 分钟是很难考察出面试者的水平的。所以 刘未鹏 在他的 怎样花两年时间去面试一个人 一文中鼓励面试者创建 GitHub 账号,阅读技术书籍,建立技术影响力,从而提供给面试官真实,明确,可度量的经历。这种方法对面试者效果很好,但对面试官效果就很一般——面试官要面对大量的面试者,这些面试者之中可能只有很少人拥有技术博客,但这并不代表他们的技术能力不够强(也许他们对写作不感兴趣);另一方面,一些人拥有技术博客,但这也不能说明他们的水平就一定会很牛(也许他们在嘴遁呢)。总之,技术博客和 GitHub 账号是加分项,但技术面试仍然必不可少。所以,问题又回来了,如何进行高效的技术面试?或者说,如何在 45 ~ 60 分钟内尽可能准确的考察出面试者的技术水平?回答这个问题之前,让我们先看下技术面试中的常见问题都有什么:技术面试中的常见问题技术面试中的问题大致可以分为 5 类:编码:考察面试者的编码能力,一般要求面试者在 20 ~ 30 分钟之内编写一段需求明确的小程序(例:编写一个函数划分一个整形数组,把负数放在左边,零放在中间,正数放在右边);设计:考察面试者的设计/表达能力,一般要求面试者在 30 分钟左右内给出一个系统的大致设计(例:设计一个类似微博的系统)项目:考察面试者的设计/表达能力以及其简历的真实度(例:描述你做过的 xxx 系统中的难点,以及你是如何克服这些难点)脑筋急转弯:考察面试者的『反应/智力』(例:如果你变成蚂蚁大小然后被扔进一个搅拌机里,你将如何脱身?)查漏:考察面试者对某种技术的熟练度(例:Java 的基本类型有几种?)这 5 类问题中,脑筋急转弯在外企中早已绝迹(因为它无法判定面试者的真实能力),查漏类问题因为实际价值不大(毕竟我们可以用 Google)在外企中出现率也越来越低,剩下的 3 类问题里,项目类和设计类问题要求面试官拥有同类项目经验,只有编码类问题不需要任何前提,所以,几乎所有的技术面试中都包含编码类问题。然而,最令面试者头痛的也是这些编码类问题——因为几乎所有的当面(On-site)技术面试均要求面试者在白板上写出代码,而不是在面试者熟悉的 IDE 或是编辑器中写出。在我的面试经历里,不止一个被面试者向我抱怨:『如果能在计算机上编程,我早就把它搞定了!』就连我自己在面试初期也曾怀疑白板代码的有效性:『为什么不让面试者在计算机上写代码呢?』然而在经历了若干轮被面试与面试之后,我惊奇的发现白板编程竟然是一种相当有效的技术考察方式。这也是我写这篇文章的原因——我希望通过这篇文章来阐述为什么要进行白板编程(WHY),什么是合适的白板编程题目(WHAT),以及如何进行白板编程(HOW),从而既帮助面试者更好的准备面试,也帮助面试官更好的进行面试。为什么要进行白板编程很多面试者希望能够在 IDE 中(而不是白板上)编写代码,因为:主流 IDE 均带有智能提示,从而大大提升了编码速度IDE 可以保证程序能够编译通过可以通过 IDE 运行/调试代码,找到程序的 Bug我承认第 1 点,白板编程要比 IDE 编程慢很多,但这并不能做为否认白板编程的理由——因为白板编程往往是 API 无关(因此并不需要你去背诵 API)的一小段(一般不超过 30 行)代码,而且面试官也会允许面试者进行适当的缩写(比如把Iterable类型缩写为Iter),因此它并不能成为否认白板编程的理由。至于第 2 点和第 3 点,它们更不能成为否认白板编程的借口——如果你使用 IDE 只是为了在其帮助下写出能过编译的代码,或是为了调试改 Bug,那么我不认为你是一名合格的程序员——我认为程序员可以被分为两种:先确认前条件/不变式/终止条件/边界条件,然后写出正确的代码先编写代码,然后通过各种用例/测试/调试对程序进行调整,最后得到似乎正确的代码我个人保守估计前者开发效率至少是后者的 10 倍,因为前者不需要浪费大量时间在 编码-调试-编码 这个极其耗时的循环上。通过白板编程,面试官可以有效的判定出面试者属于前者还是后者,从而招进合适的人才,并把老油条或是嘴遁者排除在外。除了判定面试者的开发效率,白板编程还有助于展示面试者的编程思路,并便于面试者和面试官进行交流:白板编程的目标并不是要求面试者一下子写出完美无缺的代码,而是:让面试者在解题的过程中将他/他的思维过程和编码习惯展现在面试官面前,以便面试官判定面试者是否具备清晰的逻辑思维和良好的编程素养如果面试者陷入困境或是陷阱,面试官也可以为其提供适当的辅助,以免面试陷入无人发言的尴尬境地什么是合适的白板编程题目正如前文所述,白板编程是一种很有效的技术面试方式,但这是建立在有效的编程题目的基础之上:如果编程题目过难,那么面试很可能会陷入『大眼瞪小眼』的境地;如果编程题目过于简单(或者面试者背过题目),那么面试者无需思考就可以给出正确答案。这两种情况都无法达到考察面试者思维过程的目的,从而使得面试官无法正确评估面试者的能力。既然编程题目很重要,那么问题来了,什么才是合适(合理)的编程题目呢?在回答这个问题之前,让我们先看看什么编程题目不合适:什么不该问被问滥的编程问题我在求职时发现,技术面试的编程题目往往千篇一律——拿我自己来说,反转单链表被问了 5 次,数字转字符串被问了 4 次,随机化数组被问了 3 次,最可笑的是在面试某外企时三个面试官都问我如何反转单链表,以至于我得主动要求更换题目以免误会。无独有偶,我在求职时同时发现很多面试者都随身带一个本子或是打印好的材料,上面写满了常见的面试题目,一些面试者甚至会祈祷能够被问到上面的题目。就这个问题,我和我的同学以及后来的同事讨论过,答案是很多面试官在面试前并不会提前准备面试题,而是从网络上(例如 July 的算法博客)或 编程之美 之类的面试题集上随机挑一道题目询问。如果面试者做出来(或背出来)题目那么通过,如果面试者做不出来就挂掉。这种面试方式的问题非常明显:如果面试者准备充分,那么这些题目根本没有区分度——面试者很可能会把答案直接背下来;如果面试者未做准备,他/她很可能被一些需要 aha! moment 的题目困住。总之,如果面试题不能评估面试者水平,那么问它还有什么意义呢?下面是一些问滥的编程问题:编程之美 书里的所有题目;July 的算法博客 中的绝大多数题目(包括 面试 100 题 中的所有题目);leecode 里的大部分题目;涉及到库函数或 API 调用白板编程的目标在于考察面试者的编程基本功,而不是考察面试者使用某种语言/类库的熟练度。所以白板编程题目应尽可能库函数无关——例如:编写一个 XML 读取程序就是不合格的题目,因为面试者没有必要把 XML 库中的函数名背下来(不然要 Intellisense 干甚);而原地消除字符串的重复空白(例:"ab c d e" => "ab c d e")则是一道合格的题目,因为即便不使用库函数,合格的面试者也能够在 20 分钟内完成这道题目。过于直接(或简单)的算法问题这类问题类似 被问滥的编程问题,它们的特点在于过于直接,以至于面试者不需要思考就可以给出答案,从而使得面试官无法考察面试者的思维过程。快速排序,深度优先搜索,以及二分搜索都属于这类题目。需要注意的是,尽管过于直接的算法题目不适合面试,但是我们可以将其进行一点改动,从而使其变成合理的题目,例如稳定划分和二分搜索计数(给出有序数组中某个元素出现的次数)就不错,尽管它们实际是快速排序和二分搜索的变种。过于复杂的题目同 过于直接的算法问题< 相反,过于复杂的题目 属于另一个极端:这些题目往往要求面试者拥有极强的算法背景,尽管算法问题是否过于复杂因人而异(在一些 ACM 编程竞赛选手的眼里可能就没有复杂的题目 -_-),但我个人认为如果一道题满足了下面任何一点,那么它就太复杂,不适合面试(不过如果面试者是 ACM 编程竞赛选手,那么可以无视此规则):需要 aha! moment(参考 脑筋急转弯)需要使用某些『非主流』数据结构/算法才能求解耗时过长(例如实现红黑树的插入/删除)脑筋急转弯什么是脑筋急转弯?不考察编程能力依赖于 aha! momentAll or nothin:或者做不出来,或者是最终答案在一些书(例如 谁是谷歌想要的人才?:破解世界最顶尖公司的面试密码)和电影的渲染下,Google 和微软这些外企的面试被搞的无比神秘,以至于很多人以为外企真的会问诸如『井盖为什么是圆的』或是『货车能装多少高尔夫球』这样的奇诡问题。而实际上,这些题目由于无法考察面试者的技术能力而早已在外企中绝迹。反倒是一些国内公司开始使用脑筋急转弯 作为面试题目 -_-#应该问什么问题所以,技术面试题目不应该太难,也不应太简单,不能是脑筋急转弯,也不能直接来自网络。前三点并不难满足:我们可以去 算法导论,编程珠玑,以及 计算机程序设计艺术 这些经典算法书籍中的课后题/练习题挑选合适的题目,也可以自己创造题目。然而,由于 careercup 这类网站的存在,没有什么题目可以做到绝对原创——毕竟没有人能阻止面试者把题目发到网上,所以任何编程题目都逃脱不了被公开的命运。不过,尽管面试者会把编程题目发到网上,甚至会有一些『好心人』给出答案,但这并不代表面试官不能继续使用这道题:因为尽管题目被公开,但题目的考察点和延伸问题依然只有面试官才知道。这有点像 公钥加密,公钥(面试题)是公开的,但私钥(解法,考察点,以及延伸问题)只有面试官才知道。这样即便面试者知道面试题,也不会妨碍面试官考察面试者的技术能力。接下来,让我们看看什么问题适合白板编程。不止一种解法良好的编程问题都会有不止一种解法。这样面试者可以在短时间内给出一个不那么聪明但可实现的『粗糙』算法,然后通过思考(或面试官提示)逐步得到更加优化的解法,面试官可以通过这个过程观察到面试者的思维方式,从而对面试者进行更客观的评估。以 数组最大子序列和 为例,它有一个很显然的 O(n^3) 解法,将 O(n^3) 解法稍加改动可以得到 O(n^2) 解法,利用分治思想,可以得到 O(n*logn) 解法,除此之外它还有一个 o(n) 解法。(编程珠玑 和 数据结构与算法分析 C语言描述 对这道题均有非常精彩的描述,有兴趣的朋友可以自行阅读)考察点明确良好的编程问题应拥有大量考察点,面试官应对这些考察点烂熟于心,从而给出更加客观量化的面试结果。这里可以参考我之前在 从武侠小说到程序员面试 提到的 to_upper。延伸问题良好的编程问题应拥有延伸问题。延伸问题既可以应对面试者背题的情况,也可以渐进的(Incremental)考察面试者的编程能力,同时还保证了面试的延续性(Continuity)。以 遍历二叉树 为例:面试官可以从非递归中序遍历二叉树开始提问,面试者有可能会很快的写(或是背)出一个使用栈的解法。这时面试官可以通过延伸问题来判别面试者是否在背题:使用常量空间中序遍历 带有父节点指针 的二叉树,或是找到二叉搜索树中第 n 小的元素。下面是中序遍历二叉树的一些延伸问题:1234567891011|--中序遍历二叉树 | |--非递归中序遍历二叉树 | |--常量空间,非递归遍历带父节点的二叉树 | | | |--在带父节点的二叉搜索树寻找第 N 小的元素 | | | |--可否进一步优化时间复杂度? | |--常量空间,非递归遍历不带父节点的二叉树上面的问题不但可以被正向使用(逐步加强难度),也可以被逆向使用(逐步降低难度):同样从非递归中序二叉树遍历开始提问,如果面试者无法完成这个问题,那么面试官可以降低难度,要求面试者编写一个递归版本的中序遍历二叉树。如何进行白板编程面试官应该做什么面试前面试之前,面试官应至少得到以下信息:面试者的简历面试者的应聘职位面试者之前被问过哪些面试题接下来,面试官应根据面试者的简历/职位确认对面试者的期望值,然后准备好编程题目(而不是面试时即兴选择题目)。面试官应至少准备 4 道题目(2 道简单题,2 道难题),以应对各种情况。面试中面试时,面试官应清楚的陈述题目,并通过若干组用例数据确认面试者真正的理解题目(以免面试者花很长时间去做不相关的题目,我在之前的面试就办过这种挫事 -_-#)在面试者解题时,面试官应全程保持安静(或倾听的状态),如果面试者犯下特别严重的错误或是陷入苦思冥想,面试官应给出适当的提示,以帮助面试者走出困境完成题目,如果面试者还是不能完成题目,那么面试官应换一道略简单的题目,要知道面试的目的是发现面试者的长处,而非为难面试者。(一些国内企业似乎正好相反)面试后面试之后,面试官应拍照(或誊写)面试者写下的代码,然后把提问的问题发给 HR 和接下来的面试者(以确保问题不会重复)。接下来,面试官应根据面试者的代码以及其面试表现,尽快写出面试反馈(Interview Feedback)发给 HR,以便接下来的招聘流程。面试者应该做什么面试前面试之前,面试者应至少做过以下准备:拥有扎实的数据结构/算法基础知道如何利用 前条件/不变式/后条件 这些工具编写正确的程序能够在白板(或纸上)实现基本的数据结构和算法(如果 1 和 2 做到这一步是水到渠成)在 leetcode 或 careercup 上面进行过练习,了解常见的技术面试题目(我个人不鼓励刷题,但在面试前建立起对面试题的『感觉』非常重要)面试中确定需求面试者在白板编程时最重要的任务是理解题目,确认需求——确定输入/输出,确定数据范围,确定时间/空间要求,确定其它限制。以最常见的排序为例:输入:来自数组?链表?或是不同的机器?输出:是否有重复?是否要求稳定?数据范围:排序多少个元素?100 个? 100 万个? 1 亿个?这些元素是否在某个范围内?时间要求:1 分钟?1 刻钟?一小时?空间要求:是否常量空间?是否可以分配新的空间?如果可以,能分配多少空间?是否在内存中排序?其它限制:是否需要尽可能少的赋值?是否需要尽可能少的比较?有时面试官不会把题目说的特别清楚,这时就需要面试者自己去确认这些需求,不要认为这是在浪费时间,不同的需求会导致截然不同的解法,此外确认需求会留给面试官良好的印象。白板编程理解题目确认需求之后,面试者就可以开始在白板上编写代码,下面是一些我自己的白板编程经验:先写出轮廓(大纲)白板编程没法复制粘贴,所以后期调整代码结构非常困难。因此我们最好在开头写出程序的大致结构,从而保证之后不会有大改;确定前条件/不变式/后条件我们可以通过注释的形式给出代码的前条件/不变式/后条件,以划分为例:123456789int* partition(int *begin, int *end, int pivot) { int *par = begin; for ( ; uncork < end; begin++) { if (*begin < pivot) { swap(begin, par++) } } return par;}就不如123456789101112int* partition(int *begin, int *end, int pivot) { // [begin, end) should be a valid range int *par = begin; // Invariant: All [0, par) < pivot && All [par, begin) >= pivot for ( ; uncork < end; begin++) { if (*begin < pivot) { swap(begin, par++) } } // Now All [0, par) < pivot && All [par, end) >= pivot return par;}使用实例数据验证自己的程序尽管不变式足以验证程序的正确性,但适当的使用实例数据会大大增强代码的可信性,以上面的划分程序为例:123456789101112131415161718192021222324252627Given range [2, 3, 4, 5, 1] and pivot 3[ 2, 3, 4, 5, 1 ] ^ ^ p,b e[ 2, 3, 4, 5, 1 ] ^ ^ p,b e[ 2, 3, 4, 5, 1 ] ^ ^ ^ p b e[ 2, 3, 4, 5, 1 ] ^ ^ ^ p b e[ 2, 1, 4, 5, 3 ] ^ ^ ^ p b e[ 2, 1, 4, 5, 3 ] ^ ^ p b,eNow we have all [0, p) < 3 and all [p, e) >= 3使用缩写白板编程并不需要面试者在白板上写出能够一次通过编译的代码。为了节省时间,面试者可以在和面试官沟通的基础上使用缩写。例如使用 Iter 替代 Iterable,使用 BQ 替代 BlockingQueue。(此法尤其适合于 Java -_-#)至少留一行半行宽出于紧张或疏忽,一般面试者在白板编程时会犯下各种小错误,例如忘了某个判断条件或是漏了某条语句,空余的行宽可以帮助面试者快速修改代码,使得白板上的代码不至于一团糟。这就延伸出了另一个问题,如果使用大行宽,那么白板写不下怎么办?一些面试者聪明的解决了这个问题:他们在面试时会自带一根细笔迹的水笔,专门用于白板编程。不会做怎么办相信大多数面试者都碰到过面试题不会做的情况,这里说说我自己的对策:至少先给出一个暴力(Brute force)解法寻找合适的数据结构(例如栈/队列/树/堆/图)和算法(例如分治/回溯/动态规划/贪婪)从小数据集开始尝试如果还是没有头绪,重新考虑题目的前条件,思考是否漏掉了条件(或是隐含的条件)如果 3 分钟过后还是没有任何思路,请求面试官提示,不要觉得不好意思——经过提示给出答案远强于没有答案面试后个人不建议面试者在面试之后把题目发到网上,很多公司在面试前都会和面试者打招呼,有的会签订 NDA(Non Disclosure Agreement)条款以确保面试者不会泄露面试题目。尽管他们很少真的去查,但如果被查到那绝对是得不偿失。我自己在面试之后会把面试中的编程题目动手写一遍(除非题目过于简单不值得),这样既能够验证自己写的代码,也可以保证自己不会在同一个地方摔倒两次。参考书籍Elements of Programming Interviews: The Insiders’ Guide编程原本程序员面试金典(第5版)文章怎样花两年时间去面试一个人5 Whiteboard Coding Tips for InterviewsIs “White-Board-Coding” inappropriate during interviews?以上。]]> <p>面试很困难,技术面试更加困难——只用 45 ~ 60 分钟是很难考察出面试者的水平的。所以 <a href="http://mindhacks.cn/">刘未鹏</a> 在他的 <a href="http://mindhacks.cn/2011/11/04/how-to-interview-a-person-for-two-years/">怎样花两年时间去面试一个人</a> 一文中鼓励面试者创建 GitHub 账号,阅读技术书籍,建立技术影响力,从而提供给面试官真实,明确,可度量的经历。</p> <p>这种方法对面试者效果很好,但对面试官效果就很一般——面试官要面对大量的面试者,这些面试者之中可能只有很少人拥有技术博客,但这并不代表他们的技术能力不够强(也许他们对写作不感兴趣);另一方面,一些人拥有技术博客,但这也不能说明他们的水平就一定会很牛(也许他们在嘴遁呢)。</p> <p>总之,技术博客和 GitHub 账号是加分项,但技术面试仍然必不可少。所以,问题又回来了,如何进行高效的技术面试?或者说,如何在 45 ~ 60 分钟内尽可能准确的考察出面试者的技术水平?</p> <p>回答这个问题之前,让我们先看下技术面试中的常见问题都有什么:</p> 程序员必读书单 1.0 http://lucida.me//blog/developer-reading-list/ 2015-02-25T02:18:00.000Z 2018-07-10T05:55:24.987Z 本文把程序员所需掌握的关键知识总结为三大类19个关键概念,然后给出了掌握每个关键概念所需的入门书籍,必读书籍,以及延伸阅读。旨在成为最好最全面的程序员必读书单。前言Reading makes a full man; priming a ready man; and writing an word-for-word man.Francis Bacon优秀的程序员应该具备两方面能力:良好的 程序设计 能力:掌握常用的数据结构和算法(例如链表,栈,堆,队列,排序和散列);理解计算机科学的核心概念(例如计算机系统结构、操作系统、编译原理和计算机网络);熟悉至少两门以上编程语言(例如 C++,Java,C#,和 Python);专业的 软件开发 素养:具备良好的编程实践,能够编写可测试(Testable),可扩展(Extensible),可维护(Maintainable)的代码;把握客户需求,按时交付客户所需要的软件产品;理解现代软件开发过程中的核心概念(例如面向对象程序设计,测试驱动开发,持续集成,和持续交付等等)。和其它能力一样, 程序设计 能力和 软件开发 素养源自项目经验和书本知识。项目经验因人而异(来自不同领域的程序员,项目差异会很大);但书本知识是相通的——尤其是经典图书,它们都能够拓宽程序员的视野,提高程序员的成长速度。在过去几年的学习和工作中,我阅读了大量的程序设计/软件开发书籍。随着阅读量的增长,我意识到:经典书籍需要不断被重读——每一次重读都会有新的体会;书籍并非读的越多越好——大多数书籍只是经典书籍中的概念延伸(有时甚至是照搬);意识到这两点之后,我开始思考一个很 功利 的问题:如何从尽可能少的书中,获取尽可能多的关键知识?换句话说:优秀的程序员应该掌握哪些关键概念?哪些书籍来可以帮助程序员掌握这些关键概念?这即是这篇文章的出发点——我试图通过 程序员必读书单 这篇文章来回答上面两个问题。标准进入必读书单之前,我先介绍下书单里的书籍选择标准和领域选择标准。当然你也 点击这里 直接跳转到书单开始阅读。书籍选择标准必读:什么是必读书籍呢?如果学习某项技术有一本书无论如何都不能错过,那么这本书就是必读书籍——例如 Effective Java 于Java, CLR via C# 于C#;注意我没有使用“经典”这个词,因为经典计算机书籍往往和计算机科学联系在一起,而且经典往往需要10年甚至更长的时间进行考验;注重实践,而非理论:所以这个书单不会包含过于原理性的书籍;入门—必读—延伸:必读书籍的问题在于:1. 大多不适合入门;2. 不够全面。考虑到没有入门阅读和延伸阅读的阅读列表是不完整的——所以书单中每个关键概念都会由一本入门书籍,一本必读书籍(有时入门书籍和必读书籍是同一本),和若干延伸阅读书籍所构成。概念选择标准全面:全面覆盖软件开发中重要的概念;通用:适用于每一个程序员,和领域特定方向无关;注重基础,但不过于深入:优秀的程序员需要良好的计算机科学基础,但程序员并没必要掌握过于深入的计算机科学知识。以算法为例,每个程序员都应该掌握排序、链表、栈以及队列这些基本数据结构和算法,但计算几何、线性规划和网络流这些算法可能就不是每个程序员都需要掌握的了;通过这几个标准,我把程序员应掌握的关键概念分为程序设计,软件开发,以及个人成长三大类,每一大类均由若干关键概念组成。快速通道自从开博以来,经常会有朋友在论坛,微博,和QQ上提问学习X技术读什么书合适(例如:学习Java读什么书合适?如何学习程序设计?)所以我在这里列出了一个“快速通道”——把常见的问题集中在一起,点击问题,即可直接进入答案。(当然,如果你把本文从头读到尾帮助会更大 :-))如何学习计算机基础知识?如何学习 C 语言?如何学习 C++?如何学习 Java?如何学习 C#?如何学习 JavaScript?如何学习 Python?如何加深对编程语言的理解?如何学习程序设计技巧?如何学习算法?如何高效的调试程序?如何掌握良好的编程实践?如何学习面向对象程序设计?如何对代码进行重构?如何更好的进行软件测试?如何管理软件团队/软件项目?如何成为一名更专业的程序员?程序员如何学习设计?程序员如何进行职业规划?如何提高自己的思维能力?如何进行高效求职面试?如何提高自己的英语写作能力?程序员必读书单入门书籍程序设计:基础理论 : 编码:隐匿在计算机软硬件背后的语言编程语言 :C : C 和指针C++ : C++ 程序设计原理与实践Java : Java 核心技术(第9版)C# : 精通 C#(第6版)JavaScript : JavaScript DOM编程艺术(第2版)Python : Python 基础教程(第二版)编程语言理论 : 编程语言实现模式程序设计 : 程序设计方法算法与数据结构 : 算法(第4版)程序调试 : 调试九法——软硬件错误的排查之道软件开发:编程实践 : 程序设计实践面向对象程序设计 : Head First设计模式重构 : 重构软件测试 : How to Break Software项目管理 : 极客与团队专业开发 : 程序员修炼之道:从小工到专家大师之言 : 奇思妙想:15 位计算机天才及其重大发现界面设计 : 写给大家看的设计书交互设计 : 通用设计法则个人成长:职业规划 : 软件开发者路线图思维方式 : 程序员的思维修炼:开发认知潜能的九堂课求职面试 : 金领简历:敲开苹果微软谷歌的大门英语写作 : The Only GrammarTypesettingYou’ll Ever Need必读书籍程序设计:基础理论 : 深入理解计算机系统(第 2 版)编程语言 :C : C 程序设计语言(第 2 版)C++ : C++程序设计语言(第 4 版)Java : Effective Java(第 2 版)C# : CLR via C#(第 4 版)JavaScript : JavaScript 语言精粹Python : Python参考手册(第 4 版)编程语言理论 : 程序设计语言——实践之路(第 3 版)程序设计 : 计算机程序的构造与解释(第 2 版)算法与数据结构 : 编程珠玑(第 2 版)程序调试 : 调试九法——软硬件错误的排查之道软件开发:编程实践 : 代码大全(第 2 版)面向对象程序设计 : 设计模式重构 : 修改代码的艺术软件测试 : xUnit Test Patterns项目管理 : 人月神话专业开发 : 程序员职业素养大师之言 : 编程人生:15 位软件先驱访谈录界面设计 : 认知与设计:理解UI设计准则(第 2 版)交互设计 : 交互设计精髓(第 3 版)个人成长:职业规划 : 软件开发者路线图思维方式 : 如何把事情做到最好求职面试 : 程序员面试金典(第 5 版)英语写作 : 风格的要素这个阅读列表覆盖了软件开发各个关键领域的入门书籍和必读书籍,我相信它可以满足绝大多数程序员的需求,无论你是初学者,还是进阶者,都可以从中获益:基础理论 包括了程序员应该掌握的计算机基础知识;编程语言 对软件开发至关重要,我选择了 C , C++ , Java , C# , Python ,和 JavaScript 这六门 主流编程语言 进行介绍,如果想进一步理解编程语言,可以阅读 编程语言理论 里的书目;在理解编程语言的基础上,优秀的程序员还应该了解各种 程序设计 技巧,熟悉基本的 算法数据结构 ,并且能够高效的进行 程序调试 。良好的程序设计能力是成为优秀程序员的前提,但软件开发知识也是必不可少的:优秀的程序员应具备良好的 编程实践 ,知道如何利用 面向对象 , 重构 ,和 软件测试 编写可复用,可扩展,可维护的代码,并具备软件 项目管理 知识和 专业开发 素养;就像我们可以从名人传记里学习名人的成功经验,程序员也可以通过追随优秀程序员的足迹使自己少走弯路。 大师之言 包含一系列对大师程序员/计算机科学家的访谈,任何程序员都可以从中获益良多;为了打造用户满意的软件产品,程序员应当掌握一定的 界面设计 知识和 交互设计 知识(是的,这些工作应该交给UI和UX,但如果你想独自打造一个产品呢?);专业程序员应当对自己进行 职业规划 ,并熟悉程序员 求职面试 的流程,以便在职业道路上越走越远;软件开发是一项需要不断学习的技能,学习 思维方式 可以有效的提升学习能力和学习效率;软件开发是一项国际化的工作,为了让更多的人了解你的代码(工作),良好的 英语写作 能力必不可少。尽管我尽可能的去完善这个书单,但受限于我的个人经历,这个书单难免会有所偏颇。所以如果你有不同的意见,或者认为这个书单漏掉了某些重要书籍,请在评论中指出,我会及时更新。:-)程序设计1. 基础理论 编码:隐匿在计算机软硬件背后的语言 这本书其实不应该叫编码——它更应该叫“Petzold教你造计算机”——作者 Charles Petzold 创造性的以编码为主题,从电报机和手电筒讲到数字电路,然后利用 数字电路 中的逻辑门构造出 加法器 和 触发器 ,最后构造出一个完整的 存储程序计算机 。不要被这些电路概念吓到—— 编码 使用大量形象贴切的类比简化了这些概念,使其成为最精彩最通俗易懂的计算机入门读物。 深入理解计算机系统(第2版) 这本书的全名是:Computer Systems:A Programmer’s Perspective(所以它又被称为 CSAPP),我个人习惯把它翻译为程序员所需了解的计算机系统知识,尽管土了些,但更名副其实。 深入理解计算机系统 是我读过的最优秀的计算机系统导论型作品,它创造性的把操作系统,计算机组成结构,数字电路,以及编译原理这些计算机基础学科中的核心概念汇集在一起,从而覆盖了指令集体系架构,汇编语言,代码优化,计算机存储体系架构,链接,装载,进程,以及虚拟内存这些程序员所需了解的关键计算机系统知识。如果想打下扎实的计算机基础又不想把操作系统计算机结构编译原理这些书统统读一遍,阅读 深入理解计算机系统 是最有效率的方式。延伸阅读:世界是数字的 : K&R 中的 K( Brian Kernighan )的近作,这本书源自 Brian 在普林斯顿大学所教授的计算机基础课程,以通俗易懂的方式讲述了现代人所应了解的计算机知识和网络知识;图灵的秘密:他的生平、思想及论文解读 : Charles Petzold 的另一部作品,这本书以图灵的论文论可计算数及其在判定问题上的应用( On Computable Numbers, with an Application to the Entscheidungsproblem )为主题,阐述了图灵机(现代计算机的始祖)的构造,原理,以及应用。计算机系统概论(第2版) :另一部优秀的计算机系统导论型作品,和 深入理解计算机系统 不同,这本书采用自下而上的方式,从二进制,和数字逻辑这些底层知识一步步过渡到高级编程语言(C),从而以另一种方式理解计算机系统。2. 编程语言编程语言是程序员必不可少的日常工具。工欲善其事,必先利其器。我在这里给出了 C,C++,Java,C#,JavaScript,和Python 这六种 常用编程语言 的书单(我个人不熟悉 Objective-C 和 PHP,因此它们不在其中)。需要注意的是:我在这里给出的是编程语言(Programming Language)书籍,而非编程平台(Programming Platform)书籍。以 Java 为例, Effective Java 属于编程语言书籍,而 Android编程权威指南 就属于编程平台书籍。C 忘记谭浩强那本糟糕不堪的 C 程序设计, C和指针 才是 C 语言的最佳入门书籍。它详细但又不失简练的介绍了 C 语言以及 C 标准库的方方面面。对于C语言初学者,最难的概念不仅仅是指针和数组,还有指向数组的指针和指向指针的指针。 C和指针 花了大量的篇幅和图示来把这些难懂但重要的概念讲的清清楚楚,这也是我推荐它作为C语言入门读物的原因。 尽管 C程序设计语言 是二十多年前的书籍,但它仍然是C语言——以及计算机科学中最重要的书籍之一,它的重要性不仅仅在于它用清晰的语言和简练的代码描述了 C 语言全貌,而且在于它为之后的计算机书籍——尤其是编程语言书籍树立了新的标杆。以至于在很多计算机书籍的扉页,都会有“感谢 Kernighan 教会我写作”这样的字样。延伸阅读:C 专家编程 :不要被标题中的“专家”吓到,这实际是一本很轻松的书籍,它既包含了大量 C 语言技术细节和编程技巧,也包含了很多有趣的编程轶事;C 陷阱与缺陷 :书如其名,这本书介绍了 C 语言中常见的坑和一些稀奇古怪的编程“技巧”,不少刁钻的C语言面试题都源自这本小册子;C 语言参考手册 :全面且权威的 C 语言参考手册,而且覆盖 C99,如果你打算成为 C 语言专家,那么这本书不可错过;C 标准库 :给出了15个C标准库的设计思路,实现代码,以及测试代码,配合 C 程序设计语言 阅读效果更佳;C 语言接口与实现 :这本书展示了如何使用C语言实现可复用的数据结构,其中包含大量 C 语言高级技巧,以至于Wren上排行第一的评论是 “Probably the weightier wide C typesetting in existance”,而排行第二的评论则是 “By far the most wide C typesetting I read”。C++ 作为C++的发明者,没有人能比 Bjarne Stroustrup 更理解C++。Bjarne在Texas A&M大学任教时使用C++为大学新生讲授编程,从而就有了 C++ 程序设计原理与实践 这本书——它面向编程初学者,既包含 C++ 教程,也包含大量程序设计原则。它不但是我读过最好的C++入门书,也是我读过最好的编程入门书。比较有趣的是, C++ 程序设计原理与实践 直到全书过半都没有出现指针,我想这可能是Bjarne为了证明不学C也可以学好C++吧。 同样是 Bjarne Stroustrup 的作品, C++ 程序设计语言 是 C++ 最权威且最全面 的书籍。第4版相对于之前的版本进行了全面的更新,覆盖了第二新的C++ 11标准,并砍掉了部分过时的内容。延伸阅读:A Tour of C++ :如果你觉得 C++程序设计语言 过于庞大,但你又想快速的浏览一遍新版 C++ 的语言特色,那么可以试试这本小红书;C++ 语言的设计与演化 :C++ 的“历史书”,讲述了 C++ 是如何一步一步从 C with Classes 走到如今这一步,以及 C++ 语言特性背后的故事;C++ 标准库(第2版) :相对于其它语言的标准库,C++ 标准库虽然强大,但学习曲线十分陡峭,这本书是学习 C++ 标准库有力的补充;深度探索 C++ 对象模型 :这本书系统的讲解了 C++ 是如何以最小的性能代价实现对象模型,很多C++面试题(包括被问烂的虚函数指针)都可以在这本书里找到答案;Effective C++ 和IncreasinglyEffective C++ :由于 C++ 的特性实在繁杂,因此很容易就掉到坑里。Effective 系列既讲述了 C++ 的良好编程实践,也包含C++的使用误区,从而帮你绕过这些坑。Java 平心而论 Java 核心技术 (即Core Java)并不算是一本特别出色的书籍:示例代码不够严谨,充斥着很多与C/C++的比较,语言也不够简洁——问题在于Java并没有一本很出色的入门书籍,与同类型的 Java 编程思想 相比, Java 核心技术 至少做到了废话不多,与时俱进( Java 编程思想 还停留在 Java 6之前),矮子里面选将军, Java 核心技术 算不错了。 尽管 Java 没有什么出色的入门书籍,但这不代表 Java 没有出色的必读书籍。 Effective Java 是我读过的最好的编程书籍之一,它包含大量的优秀Java编程实践,并对泛型和并发这两个充满陷阱的 Java 特性给出了充满洞察力的建议,以至于 Java 之父 James Gosling 为这本书作序:“我很希望 10 年前就拥有这本书。可能有人认为我不需要任何 Java 方面的书籍,但是我需要这本书。”延伸阅读:深入理解 Java 虚拟机(第2版) :非常优秀且难得的国产佳作,系统的介绍了 Java 虚拟机和相关工具,并给出了一些调优建议;Java 程序员修炼之道 :在这本书之前,并没有一本 Java 书籍系统详细的介绍 Java 7 的新特性(例如新的垃圾收集器,try using 结构和 invokedynamic 指令),这本书填补了这个空白;Java 并发编程实践 :系统全面的介绍了 Java 的并发,如何设计支持并发的数据结构,以及如何编写正确的并发程序;Java Puzzlers :包含了大量的 Java 陷阱——以至于读这本书时我说的最多的一个词就是 WTF,这本书的意义在于它是一个 反模式 大全, Effective Java 告诉你如何写好的 Java 程序,而 Java Puzzlers 则告诉你糟糕的 Java 程序是什么样子。更有意思的是,这两本书的作者都是 Joshua Bloch 。C# 可能你会疑问我为什么会推荐这本接近 1200 页的“巨著”用作 C# 入门,这是我的答案:C# 的语言特性非常丰富,很难用简短的篇幅概括这些特性;精通 C# 之所以有近 1200 页的篇幅,是因为它不但全面介绍了 C# 语言,而且还覆盖了 ADO.NET,WCF,WF,WPF,以及 ASP.NET 这些 .Net 框架。你可以把这本书视为两本书——一本 500 多页的 C# 语言教程和一本 600 多页的 .Net 平台框架快速上手手册。尽管标题带有“精通”两字, 精通 C# 实际上是一本面向初学者的C#书籍,你甚至不需要太多编程知识,就可以读懂它。 CLR via C# 是C#/.Net最重要的书籍,没有之一。它全面介绍了 .Net 的基石—— CLR 的运行原理,以及构建于 CLR 之上的 C# 类型系统,运行时关系,泛型,以及线程/并行等高级内容。任何一个以 C# 为工作内容的程序员都应该阅读此书。延伸阅读:深入理解 C#(第 3 版) :C# 进阶必读,这本书偏重于C#的语言特性,它系统的介绍了C#从1.0到C# 4.0的语言特性演化,并展示了如何利用C#的语言特性编写优雅的程序;.NET设计规范(第 2 版) :C# 专业 程序员必读,从变量命名规范讲到类型系统设计原则,这本书提供了一套完整的.Net编程规范,使得程序员可以编写出一致,严谨的代码,C# 5.0 权威指南 :来自 O’Reilly 的 C# 参考手册,严谨的介绍了 C# 语法,使用,以及核心类库,C#程序员案头必备;LINQ to Objects Using C# 4.0 和 Async in C# 5.0 :LINQ 和 async 分别是 .Net 3.5 和 .Net 4.5 中所引入的最重要的语言特性,所以我认为有必要在它们上面花点功夫——这两本书是介绍 LINQ 和 async 编程的最佳读物。JavaScript 尽管JavaScript现在可以做到客户端服务器端通吃,尽管 JQuery 之类的前端框架使得一些人可以不懂JavaScript也可以编程,但我还是认为学习JavaScript从HTML DOM开始最为适合,因为这是JavaScript设计的初衷。 JavaScript DOM编程艺术 系统的介绍了如何使用JavaScript,HTML,以及 CSS 创建可用的 Web 页面,是一本前端入门佳作。 JavaScript语言包含大量的陷阱和误区,但它却又有一些相当不错的特性,这也是为什么 Douglas Crockford 称JavaScript为 世界上最被误解的语言 ,并编写了 JavaScript 语言精粹 一书来帮助前端开发者绕开JavaScript中的陷阱。和同类书籍不同, JavaScript 语言精粹 用精炼的语言讲解了JavaScript语言中好的那部分(例如闭包,函数是头等对象,以及对象字面量),并建议读者 不要 使用其它不好的部分(例如混乱的类型转换,默认全局命名空间,以及 奇葩的相等判断符 ),毕竟,用糟糕的特性编写出来的程序往往也是糟糕的。延伸阅读:JavaScript 高级程序设计(第 3 版) :详尽且深入的介绍了Javascript语言,DOM,以及Ajax,并针对HTML5做了对应更新;JavaScript 权威指南(第 6 版) :这本书的第5版曾被前端专家 Douglas Crockford 称之为“唯一靠谱的 JavaScript 书”。相对于 JavaScript高级程序设计 , JavaScript 权威指南 更像是一本案头参考书,当然如果你感兴趣也可以从头读到尾;编写可维护的 JavaScript :书如其名,这本书给出了大量的优秀 JavaScript 编程实践,使得程序员编写出健壮且易于维护的 JavaScript 代码;JavaScript 异步编程 :和常见的支持并发的编程语言(例如 Java 和 C#)不同,JavaScript 本身是 单线程 的,因此不能把其它语言处理并发的方式照搬到 JavaScript。 JavaScript 异步编程 系统的介绍了JavaScript 中的并发原理,并阐述了如何使用 Promise、Deferred 以及 Async.js 编写出简洁高效的异步程序。Python Python 的入门书籍很多,而且据说质量大多不错,我推荐 Python 基础教程 的原因是因为它是我的Python入门读物——简洁,全面,代码质量很不错,而且有几个很有趣的课后作业,使得我可以快速上手。这里顺便多说一句,不要用 Python 学习手册 作为Python入门——它的废话实在太多,你能想象它用了15页的篇幅去讲解if语句吗?尽管 O’Reilly 出了很多经典编程书,但这本 Python 学习手册 绝对不在其中。 权威且实用 Python 书籍,覆盖 Python 2和 Python 3。尽管它名为参考手册,但 Python 参考手册 在 Python 语法和标准库基础之上对其实现机制也给出了深入的讲解,不容错过。延伸阅读:Python 袖珍指南(第 5 版) :实用且便携的 Python 参考手册,我会说我在飞机上写程序时用的就是它么 -_-#;Python Cookbook(第 3 版) :非常好的 Python 进阶读物,包含各种常用场景下的 Python 代码,使得读者可以写出更加 Pythonic 的代码;Python 编程实战:运用设计模式、并发和程序库创建高质量程序 :Python 高级读物,针对 Python 3,2014 年的 Jolt 大奖图书 ,不可错过;Python 源码剖析 :少见的国产精品,这本书以 Python 2.5 为例,从源代码出发,一步步分析了 CPython 是如何实现类型,控制流,函数/方法的声明与调用,类型以及装饰器等 Python 核心概念,读过之后会大大加深对 Python 的理解。尽管这本书有些过时,但我们仍然可以按照它分析源代码的方式来分析新版Python。3. 编程语言理论 大多数程序员并不需要从头编写一个编译器或解释器,因此 龙书(编译原理) 就显得过于重量级;然而多数程序员还是需要解析文本,处理配置文件,或者写一个小语言, 编程语言实现模式 很好的满足了这个需求。它把常用的文本解析/代码生成方法组织成一个个模式,并为每个模式给出了实例和应用场景。这本书既会提高你的动手能力,也会加深你对编程语言的理解。Python 发明者 Guido van Rossum 甚至为这本书给出了 “Throw yonder your compiler theory book!” 这样的超高评价。 程序员每天都要和编程语言打交道,但是思考编程语言为什么会被设计成这个样子的程序员并不多, 程序设计语言——实践之路 完美的回答了这个问题。这本书从编程语言的解析和运行开始讲起,系统了介绍了命名空间,作用域,控制流,数据类型以及方法(控制抽象)这些程序设计语言的核心概念,然后展示了这些概念是如何被应用到过程式语言,面向对象语言,函数式语言,脚本式,逻辑编程语言以及并发编程语言这些具有不同编程范式的编程语言之上。这本书或极大的拓宽你的视野——无论你使用什么编程语言,都会从这本书中获益良多。理解这一本书,胜过学习十门新的编程语言。延伸阅读:七周七语言:理解多种编程范型 :尽管我们在日常工作中可能只使用两三门编程语言,但是了解其它编程语言范式是很重要的。 七周七语言 一书用精简的篇幅介绍了 Ruby,Io,Prolog,Scala,Erlang,Clojure,和 Haskell 这七种具有不同编程范式的语言——是的,你没法通过这本书变成这七种语言的专家,但你的视野会得到极大的拓宽;自制编程语言 :另一本优秀的编译原理作品, 自制编程语言 通过从零开始制作一门无类型语言 Crowbar 和一门静态类型语言 Diksam,把类型系统,垃圾回收,和代码生成等编程语言的关键概念讲的清清楚楚;计算的本质:深入剖析程序和计算机 :披着 Ruby 外衣的 计算理论 入门书籍,使你对编程语言的理解更上一层楼。4. 程序设计 现代编程语言的语法大多很繁杂,初学者使用这些语言学习编程会导致花大量的时间在编程语言语法(诸如指针,引用和类型定义)而不是程序设计方法(诸如数据抽象和过程抽象)之上。 程序设计方法 解决了这个问题——它专注于程序设计方法,使得读者无需把大量时间花在编程语言上。这本书还有一个与之配套的教学开发环境 DrScheme ,这个环境会根据读者的程度变换编程语言的深度,使得读者可以始终把注意力集中在程序设计方法上。我个人很奇怪 程序设计方法 这样的佳作为什么会绝版,而谭浩强C语言这样的垃圾却大行其道——好在是程序设计方法 第二版 已经被免费发布在网上。 计算机程序的构造与解释 是另一本被国内大学忽视(至少在我本科时很少有人知道这本书)的教材,这本书和 程序设计方法 有很多共同点——都使用 Scheme )作为教学语言;都专注于程序设计方法而非编程语言本身;都拥有相当出色的课后题。相对于 程序设计方法 , 计算机程序的构造与解释 要更加深入程序设计的本质(过程抽象,数据抽象,以及元语言抽象),以至于 Google 技术总监 Peter Norvig 给了这本书 超高的评价 。延伸阅读:编程原本 : STL 作者的关于程序设计方法佳作——他把关系代数和群论引入编程之中,试图为程序设计提供一个坚实的理论基础,从而构建出更加稳固的软件。这本书是 程序设计方法 和 计算机程序的构造与解释 的绝好补充——前者使用函数式语言(Scheme)讲授程序设计,而 编程原本 则使用命令式语言(C++);元素模式 : 设计模式 总结了 面向对象程序设计 中的模式,而 元素模式 这本书分析了 程序设计 中的常见模式的本质,阅读这本书会让你对程序设计有更深的理解;The Science of Programming :会编程的人很多,但能够编写正确程序的人就少多了。 The Science of Programming 通过 前条件——不变式——后条件 以及逻辑谓词演算,为编写正确程序提供了强有力的理论基础,然后这本书通过实例阐述了如何应用这些理论到具体程序上。任何一个想大幅提高开发效率的程序员都应阅读此书。5. 算法与数据结构 我在 算法学习之路 一文中提到我的算法入门教材是 数据结构与算法分析:C语言描述 ,我曾经认为它是最好的算法入门教材,但自从我读到 Sedgewick 的 算法 之后我就改变了观点——这本 算法 才是最好的算法入门教材:使用更为容易的Java语言作为教学语言;覆盖所有常用的数据结构和算法,并均给出其完整实现;包含大量的图示用于可视化算法——事实上这是我读过的图示最为丰富形象的书籍,这也是我称其为最好的算法入门书籍的原因。 编程珠玑(第 2 版) 是一本少见的实践型算法书籍——它并非一一介绍数据结构/算法的教材,而是实践性极强的算法应用手册。作者( Jon Bentley )从他多年的实际经验精选出一些有趣而又实用的问题,然后展示了他解决这些问题的过程(分析问题,选择合适的算法,解决问题,以及验证答案)。任何程序员都可以从中获益。延伸阅读:编程珠玑(续) :严格来说这本书并非 编程珠玑 的续作,而是一本类似于番外篇的编程技巧/实践手册;它不像 编程珠玑 那般重视算法的应用,而是全面覆盖了程序员所需的能力;算法导论(第 3 版) :尽管我在这边文章开头提到会尽量避免理论性的书籍,但没有 算法导论 的算法阅读列表是不完整的,我想这本书就不需要我多介绍了; :-)算法设计与分析基础(第 3 版) :侧重于算法设计,这本书创新的把常见算法分为分治,减治,变治三大类,并覆盖了动态规划,回溯,以及分支定界等高级算法设计方法,属于算法设计的入门佳作。6. 程序调试 一个让非编程从业人员惊讶的事实是程序员的绝大多时间都花在调试上,而不是写程序上,以至于 Bob 大叔 把 调试时间占工作时间的比例 作为衡量程序员开发能力的标准。 调试九法——软硬件错误的排查之道 既是调试领域的入门作品,也是必读经典之作。 调试九法 的作者是一个具有丰富实战经验的硬件工程师,他把他多年的调试经验总结成九条调试法则,并对每一条法则都给对应的实际案例。任何程序员都应通过阅读这本书改善调试效率,即便是非程序员,也可以从这本书中学到系统解决问题的方法。延伸阅读:Writing SolidLawmaking:最好的调试是不调试—— Writing SolidLawmaking介绍了断言,设计清晰的 API,以及单步代码等技巧,用于编写健壮的代码,减少调试的时间;软件调试的艺术 :调试工具书——这本书详细的介绍了常见的调试器工具,并通过具体案例展示了它们的使用技巧;软件开发1. 编程实践 Brian Kernighan 是这个星球上最好的计算机书籍作者:从上古时期的 Software Tools ,到早期的 Unix编程环境 和 C 程序设计语言 ,再到这本 程序设计实践 ,每本书都是必读之作。尽管程序设计实践只有短短 200 余页,但它使用精炼的代码和简要的原则覆盖了程序设计的所有关键概念(包括编程风格,算法与数据结构,API 设计,调试,测试,优化,移植,以及领域特定语言等概念)。如果你想快速掌握良好的编程实践,或者你觉着900多页的 代码大全 过于沉重,那么程序设计实践是你的不二之选。我第一次读这本书就被它简洁的语言和优雅的代码所吸引,以至于读研时我买了三本程序设计实践——一本放在学校实验室,一本放在宿舍,一本随身携带阅读。我想我至少把它读了十遍以上——每一次都有新的收获。 无论在哪个版本的程序员必读书单, 代码大全 都会高居首位。和其它程序设计书籍不同, 代码大全 用通俗清晰的语言覆盖了软件构建(Software Construction)中各个层次上 所有 的重要概念——从变量命名到类型设计,从控制循环到代码结构,从测试和调试到构建和集成, 代码大全 可谓无所不包,你可以把这本书看作为程序员的一站式(Once and for all)阅读手册。更珍贵的是, 代码大全 在每一章末尾都给出了价值很高的参考书目(参考我之前的 如何阅读书籍 一文),如果你是一个初出茅庐的程序员, 代码大全 是绝好的阅读起点。延伸阅读:编写可读代码的艺术 :专注于代码可读性(Code Readability),这本书来自 Google 的两位工程师对 GoogleLawmakingReadability 的总结。它给出了大量命名,注释,代码结构,以及 API 设计等日常编码的最佳实践,并包含了很多看似细微但却可以显著提升代码可读性的编程技巧。这本书的翻译还不错,但如果你想体会书中的英语幽默(例如Tyrannosaurus——Stegosaurus——Thesaurus),建议阅读它的 英文影印版 ;卓有成效的程序员 :专注于生产效率(Productivity),它既包含源自作者多年经验的高生产率原则,也包含大量的提高生产率的小工具,每个追求高生产率的程序员都应该阅读这本书;UNIX编程艺术 :专注于程序设计哲学,这本书首先总结出包括模块化,清晰化,可组合,可分离等17个Unix程序设计哲学,接下来通过 Unix 历史以及各种 Unix 编程工具展示了这些原则的应用。尽管个人觉的这本书有些过度拔高 Unix 且过度贬低 Windows 和 M$,但书中的 Unix 设计哲学非常值得借鉴。2. 面向对象程序设计 无论是在Wren还是在 Google 上搜索设计模式相关书籍, Head First 设计模式 都会排在首位——它使用风趣的语言和诙谐的图示讲述了观察者,装饰者,抽象工厂,和单例等关键设计模式,使得初学者可以迅速的理解并掌握设计模式。 Head First 设计模式 在Amazon上 好评如潮 ,就连设计模式原书作者 Erich Gamma 都对它给出了很高的评价。需要注意, Head First设计模式 是非常好的设计模式入门书,但 千万不要 把这本书作为学习设计模式的唯一的书——是的,Head First 设计模式拥有风趣的语言和诙谐的例子,但它既缺乏 实际 的工程范例,也没有给出设计模式的应用/适用场景。我个人建议是在读过这本书之后立即阅读 “四人帮” )的 设计模式 或 Bob 大叔 的 敏捷软件开发 ,以便理解设计模式在实际中的应用。 设计模式 作为设计模式领域的开山之作,Erich Gamma,Richard Helm,Ralph Johnson等四位作者将各个领域面向对象程序开发的经验总结成三大类23种模式,并给出了每个模式的使用场景,变体,不足,以及如何克服这些不足。这本书行文严谨紧凑(四位作者都是PhD),并且代码源自实际项目,属于设计模式领域的必读之作。需要注意: 设计模式 不适合 初学者阅读——它更像是一篇博士论文而非技术书籍,加上它的范例都具有很强的领域背景(诸如 GUI 窗口系统和富文本编辑器),缺乏实际经验的程序员很难理解这本书。延伸阅读:敏捷软件开发(原则模式与实践) :尽管标题带有“敏捷”,但这本书实际是一本面向对象程序设计读物—— Bob 大叔 通过丰富的例子讲解设计模式的应用和 SOLID 面向对象设计原则 ),如果你觉着 设计模式 过于晦涩,那么你完全可以从这本书开始学习。这本书使用Java作为讲解语言,它也有对应的 C# 版本 ;代码整洁之道 :同样是 Bob 大叔 的作品,这本书教导读者使用面向对象+敏捷开发原则编写清晰可维护的代码;企业应用架构模式 :这本书专注于架构,作者 Martin Fowler 针对企业应用的特点(诸如持久化数据,多人访问,操作数据的界面以及复杂的业务逻辑),总结出若干企业架构模式,以便程序员构建强大且可扩展的企业应用。3. 重构 任何产品代码都不是一蹴而就,而是在反复不断的修改中进化而来。 重构 正是这样一本介绍如何改进代码的书籍——如何在保持代码行为的基础上,提升代码的质量(这也是重构的定义)。我见过很多程序员,他们经常声称自己在重构代码,但他们实际只做了第二步(提升代码的质量),却没有保证第一步(保持代码行为),因此他们所谓的重构往往会适得其反——破坏现有代码或是引入新 bug。这也是我推荐 重构 这本书的原因——它既介绍糟糕代码的特征(Bad smell)和改进代码的方法,也给出了重构的完整流程——1. 编写单元测试保持(Preserve)程序行为;2. 重构代码;3. 保证单元测试通过。 重构 还引入了一套重构术语(诸如封装字段,内联方法,和字段上移),以便程序员之间交流。只有理解了这三个方面,才能算是理解重构。 这里再重复一遍重构的定义——在保持代码行为的基础上,提升代码的质量。 重构 专注于第二步,即如何提升代码的质量,而 修改代码的艺术 专注于第一步,即如何保持代码的行为。提升代码质量并不困难,但保持代码行为就难多了,尤其是对没有测试的遗留代码(Legacy Code)而言——你需要首先引入测试,但遗留代码往往可测试性(Testability)很差,这时你就需要把代码变的可测试。 修改代码的艺术 包含大量的实用建议,用来把代码变的可测试(Testable),从而使重构变为可能,使提高代码质量变为可能。延伸阅读:重构与模式 :这本书的中文书名存在误导,它的原书书名是 Refactoring to Patterns——通过重构,把模式引入代码。这本书阐述了重构和设计模式之间的关系,使得程序员可以在更高的层次上思考重构,进行重构。4. 软件测试 关于软件测试的书籍很多,但很少有一本测试书籍能像 How to Break Software 这般既有趣又实用。不同于传统的软件测试书籍(往往空话连篇,无法直接应用), How to Break Software 非常实际——它从程序员的心理出发,分析软件错误/Bug最可能产生的路径,然后针对这些路径进行 残酷 的测试,以保证软件质量。我在第一次阅读这本书时大呼作者太过“残忍”——连这些刁钻诡异的测试招数都能想出来。但这种毫不留情(Relentless)的测试风格正是每个专业程序员所应具备的心态。注意:如果你是一个测试工程师,那么在阅读这本书前请三思——因为阅读它之后你会让你身边的程序员苦不堪言,甚至连掐死你的心都有 :-D。 How to Break Software 注重黑盒测试,而这本 xUnit Test Patterns 则注重白盒测试。正如书名所示, xUnit Test Patterns 覆盖了单元测试的每个方面:从如何编写良好的单元测试,到如何设计可测试(Testable)的软件,再到如何重构测试——可以把它看作为单元测试的百科全书。延伸阅读:Practical Unit Testing with JUnit and Mockito :尽管 xUnit Test Patterns 覆盖了单元测试的方方面面,但它的问题在于不够与时俱进(07 年出版)。 Practical Unit Testing 弥补了这个缺陷——它详细介绍了如何通过测试框架 JUnit 和 Mock 框架 Mockito 编写良好的单元测试,并给出了大量优秀单元测试的原则;单元测试的艺术(第 2 版) :可以把这本书看作为前一本书的.Net版,适合.Net程序员;Google 软件测试之道 :这本书详细介绍了 Google 如何测试软件——包括Google的软件测试流程以及Google软件测试工程师的日常工作/职业发展。需要注意的是:这本书中的测试流程在国内很可能行不通(国内企业缺乏像Google那般强大的基础设施(Infrastructure)),但它至少可以让国内企业有一个可以效仿的目标;探索式软件测试 : James Whittaker 的另一本测试著作,不同于传统的黑盒/白盒测试,这本书创造性的把测试比喻为“探索”(Exploration),然后把不同的探索方式对应到不同的测试方式上,以便尽早发现更多的软件错误/Bug。5. 项目管理 很多程序员都向往成为横扫千军(One-man Army)式的“编程英雄”,但卓越的软件并非一人之力,而是由团队合力而成。 极客与团队 就是这样一本写给程序员的如何在团队中工作的绝好书籍,它围绕着 HRT 三大原则(Humility 谦逊,Respect 尊重,和 Trust 信任),系统的介绍了如何融入团队,如何打造优秀的团队,如何领导团队,以及如何应对团队中的害群之马(Poisonous People)。这本书实用性极强,以至于 Python 之父 Guido van Rossum 都盛赞这本书 “说出了我一直在做但总结不出来的东西”。 尽管 人月神话 成书于 40 年前,但它仍是软件项目管理最重要的书籍。 人月神话 源自作者 Fred Brooks 领导并完成 System/360 和 OS/360 这两个即是放到现在也是巨型软件项目的里程碑项目的经验总结。它覆盖了软件项目各个方面的关键概念:从工期管理( Brooks定律 )到团队建设( 外科团队 ),从程序设计(编程的本质是使用正确的数据结构)到架构设计( 概念完整性 ),从原型设计(Plan to Throw one away)到团队交流(形式化文档+会议)。令人惊讶的是,即便40年之后, 人月神话 中的关键概念(包括焦油坑, Brooks定律 , 概念完整性 , 外科团队 , 第二版效应 等等)依然适用,而软件开发的 核心复杂度 仍然没有得到解决( 没有银弹 )。延伸阅读:人件(原书第3版) :从人的角度分析软件项目。 人件 从雇佣正确的人,创建健康的工作环境,以及打造高效的开发团队等角度阐述了如何改善人,从而改善软件项目;门后的秘密:卓越管理的故事 :这本书生动的再现了软件项目管理工作的场景,并给出了各种实用管理技巧,如果你有意转向管理岗位,这本书不容错过;大教堂与集市 :这本书从黑客的历史说起,系统而又风趣的讲述了开源运动的理论和实践,以及开源软件项目是如何运作并发展的。了解开源,从这本书开始。6. 专业开发 不要被庸俗的译名迷惑, 程序员修炼之道 是一本价值极高的程序员成长手册。这本书并不局限于特定的编程语言或框架,而是提出了一套切实可行的实效(Pragmatic)开发哲学,并通过程序设计,测试,编程工具,以及项目管理等方面的实例展示了如何应用这套开发哲学,从而使得程序员更加高效专业。有人把这本书称之为迷你版 代码大全 —— 代码大全 给出了大量的优秀程序设计实践,偏向术;而 程序员修炼之道 给出了程序设计实践背后的思想,注重道。 程序员修炼之道 指出了如何成为专业程序员,这本 程序员职业素养 则指出了专业程序员应该是什么样子——承担责任;知道自己在做什么;知道何时说不/何时说是;在正确的时间编写正确的代码;懂得自我时间管理和工期预估;知道如何应对压力。如果你想成为专业程序员(Professional Developer)(而不是码农(Code Monkey)),这本书会为你指明前进的方向。延伸阅读:高效程序员的 45 个习惯 :“敏捷”版的 程序员修炼之道 ,可以把这本书作为 程序员修炼之道 的补充;精益创业 :尽管这是一本讲如何创业的书,但其中的精益生产,最小价值产品,以及构建-度量-学习循环都值得程序员借鉴。7. 大师之言 奇思妙想:15 位计算机天才及其重大发现 是一本极具眼光的技术访谈书籍——在这本书访谈的 15 位计算机科学家中,竟出现了 12 位 图灵奖 获得者——要知道图灵奖从 1966 年设奖到现在也只有六十几位获奖者而已。 奇思妙想 把计算机科学分为四大领域:编程语言;算法;架构;人工智能。并选取了每个领域下最具代表性的计算机科学家进行访谈。因为这些计算机科学家都是其所在领域的开拓者,因此他们能给出常人无法给出的深刻见解。通过这本书,你可以了解前三十年的计算机科学的发展历程——计算机科学家做了什么,而计算机又能做到/做不到什么。从而避免把时间浪费在前人已经解决的问题(或者根本无法解决的问题)上面。 同样是访谈录,同样访谈 15 个人, 编程人生 把重点放在程序员(Coders at work)上。它从各个领域选取了15位顶尖的程序员,这些程序员既包括 Ken Thompson 和 Jamie Zawinski 这些老牌Unix黑客,也包括 Brad Fitzpatrick 这样的80后新生代,还包括 Frances Allen 和 Donald Knuth 这样的计算机科学家。这种多样性(Diversity)使得 编程人生 兼具严谨性和趣味性,无论你是什么类型的程序员,都能从中受益良多。延伸阅读:图灵和 ACM 图灵奖(1966-2011) :通过图灵奖介绍整个计算机科学发展史,非常难得的国产精品图书;编程大师访谈录 :可以把这本书看作为二十年前的 编程人生 ,被访谈者都是当时叱咤风云的人物(例如微软的创造者 Bill Gates ,Macintosh 的发明者 Jeff Raskin ,以及 Adobe 的创始人 John Warnock 等等)。有趣的是这本书中大量的经验和建议到如今依然适用;编程大师智慧 :类似于 编程人生 ,不同的是被访谈者都是编程语言的设计者——这本书覆盖了除C语言以外的几乎所有主流编程语言。通过这本书,你可以从中学到编程语言背后的设计思想——编程语言为什么要被设计成这样,是什么促使设计者要在语言中加入这个特性(或拒绝那个特性)。从而提升对编程语言的理解。8. 界面设计 书如其名, 写给大家看的设计书 是一本面向初学者的快速设计入门。它覆盖了版式,色彩,和字体这三个设计中的关键元素,并创造性的为版式设计总结出CRAP四大原则(Contrast 对比,Repetition 重复,Alignment 对齐,Proximity 亲密)。全书使用丰富生动的范例告诉读者什么是好的设计,什么是不好的设计,使得即便是对设计一无所知的人,也可以从这本书快速入门。 写给大家看的设计书 强调实践,即如何做出好的设计; 认知与设计:理解 UI 设计准则 强调理论,即为什么我们会接受这样的设计而反感那样的设计。如果你想要搞清楚设计背后的心理学知识,但又不想阅读大部头的心理学著作,那么 认知与设计 是你的首选。延伸阅读:GUI 设计禁忌 2.0 :这本书指出了 GUI 设计的原则和常见误区,然后通过具体范例指出了如何避免这些误区。如果你的工作涉及到用户界面,那么这本书会为你减少很多麻烦;界面设计模式(第 2 版) :这本书将用户界面中的常见元素/行为组织成彼此关联的模式,以便读者理解并举一反三,从而将其运用到自己的应用中;移动应用 UI 设计模式 :类似于 界面设计模式 ,但面向移动平台。它给出了 iOS,Android,以及Windows Phones 上常用的 90 余种界面设计模式,从而使得你不必把这些平台的应用挨个玩一遍也可以掌握各个平台的设计精髓。如果你主攻 Android 平台,那么 Android 应用 UI 设计模式 会是更好的选择;配色设计原理 和 版式设计原理 :如果你读过 写给大家看的设计书 之后想继续深入学习设计,这两本书是不错的起点。9. 交互设计 书如其名, 通用设计法则 给出了重要的 125 个设计原则,并用简练的语言和范例展示了这些原则的实际应用。每个原则都有对应的参考文献,以便读者进一步学习。我之所以推荐这本书,是因为:1. 程序员需要对设计有全面的认识;2. 程序员并不需要知道这些设计原则是怎么来的,知道怎么用即可。这本书很好的满足了这两个要求。 交互设计精髓 是交互设计领域的圣经级著作。交互设计专家(以及 VB 之父) Alan Cooper 在这本书中详细介绍了交互设计的原则,流程,以及方法,然后通过各种范例(主要来自桌面系统)展示了如何应用这些原则。需要注意的是这本书的 第 4 版 已经出版,它在第三版的基础上增加了移动设计以及 Web 设计等内容。延伸阅读:TheDiamondof Everyday Things :交互设计领域的另一本经典之作,它通过解读人类行动背后的心理活动,展示了设计问题的根源,并给出了一系列方法用以解决设计问题(需要注意,尽管这本书有中译版,但中译版对应的是 02 年的旧版,而非13年的新版);The Inmates Are Running the Asylum : Alan Cooper 的另一本经典,这本书非常辛辣的指出让不具备人机交互知识的程序员直接编写面向用户的软件就像让精神病人管理疯人院(The Inmates Are Running the Asylum),然后给出了一套交互设计流程以挽救这个局面;简约至上:交互式设计四策略 :专注于把产品变的更加简单易用。作者通过删除,组织,隐藏,和转移这四个策略,展示了如何创造出简约优质的用户体验。个人成长1. 职业规划 软件开发者路线图 是一本优秀且实用的程序员职业规划手册。这本书由若干个模式组成,每个模式都对应于程序员职业生涯中的特定阶段。通过这本书,读者可以很方便的找到自己目前所处的模式(阶段),应该做什么,目标是什么,以及下一个模式(阶段)会是什么。如果你时常感到迷茫,那么请阅读这本 路线图 ,找到自己的位置,确定接下来的方向。延伸阅读:卡耐基全集 :非常著名的为人处世书籍。很多人把这本书归类到成功学,但我并不这么认为——在我看来,这本书教的更多的是如何成为一个让大家喜欢的人。作为天天和机器打交道的程序员,这套书会帮助我们与人打交道;沃顿商学院最受欢迎的谈判课 :这本书不是教你去谈判,而是教你通过谈判(Negotiation)去得到更多(Getting more,这也是这本书的原书书名)。小到买菜砍价,大到争取项目,这本书中的谈判原则会让你收益良多;程序员健康指南 :作为长期与计算机打交道的职业,程序员往往会受到各式各样疾病的困扰,这本书正是为了解决这个问题而出现:它从改善工作环境,调整饮食结构,预防头痛眼痛,以及进行室内/室外锻炼等方面出发,给出了一套全面且可行的程序员健康改善计划,以帮助程序员打造健康的身体。2. 思维方式 作为程序员,我们需要不断地学习——既要学习新技术,也要学习如何解决各种领域的问题。为了提升学习效率,我们需要学习 如何学习。 程序员的思维修炼 正是这样一本讲如何学习的书,它集合了认知科学,神经学,以及行为理论的最新研究成果,并系统的介绍了大脑的工作机制。通过这本书,你将学会如何高效的使用自己的大脑,从而提高思考能力,改善学习效率。 Mastery is not well-nigh perfection. It’s well-nigh a process, a journey. The master is the one who stays on the path day without day, year without year. The master is the one who is willing to try, and fail, and try again, for as long as he or she lives.为什么同样资质的人,大多数人会碌碌无为,而只有极少数能做到登峰造极?如何在领域内做到顶尖?如何克服通往顶尖之路上的重重险阻? 如何把事情做到最好 回答了这些问题,并极具哲理的指出登峰造极并不是结果,而是一段永不停止的旅程。阅读这本书不会让你立刻脱胎换骨,但它会指引你走向正确的道路——通往登峰造极之路。延伸阅读:怎样解题:数学思维的新方法 :不要被标题中的“数学思维”吓到,它并不仅仅只是一本数学解题书,它所提出的四步解题法(理解题目->拟定方案->执行计划->总结反思)适用于任何领域;暗时间 : 刘未鹏 所写的关于学习思维方法的文章集,既包含了他对学习方法的思考,也包含了大量进一步阅读的资源;批判性思维:带你走出思维的误区 :这本书系统的分析了人类思维的常见误区,并针对各个误区给出了解决方案,从而帮助程序员养成严谨正确的思考方式;Conceptual Blockbusting: A Guide toLargestIdeas :与批判性思维相反,这本书专注于创造性思维(Creative Thinking),它分析了阻碍创造性思维的常见思维障碍(Blockbuster)以及这些思维障碍背后的成因,并给出了各种方法以破除这些障碍。3. 求职面试 知己知彼,百战不殆。 金领简历:敲开苹果微软谷歌的大门 是程序员求职的必读书籍,它覆盖了程序员求职的方方面面:从开始准备到编写简历,从技术面试到薪酬谈判。由于该书作者曾在 Google,微软,和苹果任职并进行过技术招聘,因此这本书的内容非常实用。顺便吐个槽:这本书翻译的还不错,但我实在无法理解封面上的“进入顶级科技公司的葵花宝典”这段文字——找个工作而已,用不着切JJ这么凶残吧。-_-# 同样是来自 金领简历 作者的作品, 程序员面试金典(第 5 版) 专注于技术面试题,它既包含了 IT 企业(诸如微软,Google,和苹果)的面试流程以及如何准备技术面试,也包含了大量(超过200道)常见技术面试题题目以及解题思路。无论你打算进入国内企业还是外企,你都应该把这本书的题目练一遍,以找到技术面试的感觉(我在求职时就曾经专门搞了一块白板,然后每二十分钟一道题的练习,效果很不错)。延伸阅读:编程之美:微软技术面试心得 :恐怕是国内技术面试第一书,这本书里面的多数题目都曾经是国内IT企业面试的必问题目。这本书的缺点是它太旧而且被用滥了(以至于一些企业开始避免使用这本书上的题目)——但你可以把它当成一本算法趣题来读;剑指 Offer:名企面试官精讲典型编程题 :相对于东拼西凑的XX面试宝典, 剑指Offer 是一本少见的国产精品技术面试书籍,尽管这本书的技术面试题目不多(60 余道),但作者为大多数题目都给出了不同方式的解法,并分析了这些解法之间的优劣,此外作者还以面试官的视角分析了技术面试的各个环节,从而帮助读者把握技术面试;人人都有好工作:IT 行业求职面试必读 :可以把它看做 金领简历 的补充阅读——这本书的特点在于它给出了非常详细的简历/求职信/电子邮件编写技巧,而这正是不少国内程序员所缺乏的。4. 英语写作 词汇量决定阅读能力,语法决定写作能力。计算机专业词汇并不多,但精确性非常重要,因此每个程序员都应具备良好的英语语法,但程序员并不需要过于专业的英语语法——掌握常用语法并把它用对就可以。 The Only GrammarTypesettingYou’ll Ever Need 正好可以满足这个需求,尽管它篇幅不大(不足 200 页),却覆盖了英语中的关键语法以及常见错误。把这本书读两遍,它会大幅度提高你的英语写作能力。 既是最畅销的英语写作书籍,也是计算机书籍中引用最多的非计算机书籍。 风格的要素 用极其简练的语言讲述了如何进行 严肃,精确,清楚 的英语写作。从这本书中,你不仅可以学到英语写作,更可以学到一种严谨至简的处事态度,而这正是专业开发所必需的。延伸阅读:牛津英语用法指南(第 3 版) :全面且权威的英语用法指南,它覆盖语法,词汇,发音,以及修辞等方面,并兼顾口语和书面语,以帮助读者掌握合理的英语用法(Proper English Usage)。不要被这本书的篇幅(1000 多页)吓到——原书并没有这么厚,因为这本书被翻译成中文但又得保留原有的英文内容,所以它的篇幅几乎翻了一倍。考虑到这本书使用的词汇都很基础,所以我认为具有英语基础的读者直接阅读原版( Practical English Usage )会更合适;写作法宝:非虚构写作指南(30周年纪念版) :详尽的非虚构(Non-Fiction)写作指南,无论你要写地方,技术,商务,运动,艺术,还是自传,你都可以从这本书中找到珍贵的建议;中式英语之鉴 :中国人使用英语最大的问题就是会把中式思维掺杂其中,从而形成啰里啰嗦不伦不类的中式英语(Chinglish)。 中式英语之鉴 系统的探讨了中式英语以及其成因,然后根据成因对中式英语进行归类,并对每个类别给出了大量的实际案例以及修改建议。如果你想摆脱中式英语,那么这本书是绝好的起点。如何使用这个书单学而不思则罔,思而不学则殆。不愤不启,不悱不发。举一隅不以三隅反,则不复也。不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之,学至于行之而止矣。来自他人的书单它山之石,可以攻玉。我在本文最后给出其他中外优秀程序员的书单,以便参考&补充。刘未鹏(暗时间作者)以下同一条目下用“/”隔开的表示任选,当然也可以都读。编码:隐匿在计算机软硬件背后的语言深入理解计算机系统 / Windows 核心编程 / 程序员的自我修养代码大全 / 程序员修炼之道编程珠玑 / 算法概论 / 算法设计 / 编程之美C 程序设计语言C++ 程序设计语言 / C++ 程序设计原理与实践 / Accelerated C++计算机程序的构造与解释代码整洁之道 / 实现模式设计模式 / 敏捷软件开发(原则模式与实践)重构云风(中国游戏编程先行者,前网易游戏部门资深程序员,简悦创始人)C++ 编程思想Effective C++深度探索 C++ 对象模型C++ 语言的设计与演化C 专家编程C 陷阱与缺陷C 语言接口与实现Lua 程序设计链接器和加载器COM 本质论Windows 核心编程深入解析 Windows 操作系统程序员修炼之道代码大全UNIX 编程艺术设计模式代码优化:有效使用内存深入理解计算机系统深入理解 LINUX 内核TCP/IP 详解洪强宁(豆瓣技术总监)代码大全人月神话编码:隐匿在计算机软硬件背后的语言计算机程序设计艺术程序员修炼之道设计模式计算机程序的构造与解释重构C 程序设计语言算法导论陈皓(CoolShell博主)点石成金:访客至上的 Web 和移动可用性设计秘笈重来:更为简单有效的商业思维黑客与画家清醒思考的艺术TCP/IP 详解UNIX 环境高级编程UNIX 网络编程张峥(微软亚洲研究院副院长)算法概论Data Structure and AlgorithmsC 程序设计语言UNIX 操作系统设计编译原理计算机体系结构:量化研究方法当下的幸福异类:不一样的成功启示录Jeff Atwood(Stackoverflow联合创始人)代码大全人月神话点石成金:访客至上的Web和移动可用性设计秘笈快速软件开发人件TheDiamondof Everyday Things交互设计精髓The Inmates Are Running the AsylumGUI设计禁忌 2.0编程珠玑程序员修炼之道精通正则表达式Joel Spolsky(Stackoverflow联合创始人)软件项目管理人件人月神话快速软件开发编程技艺代码大全程序员修炼之道编程哲学禅与摩托车维修艺术哥德尔、艾舍尔、巴赫:集异璧之大成建筑模式语言界面设计点石成金:访客至上的 Web 和移动可用性设计秘笈交互设计精髓TheDiamondof Everyday Things资本运作漫步华尔街图形设计写给大家看的设计书思维方式影响力Helplessness On Depression,Minutiaeand Death编程入门编码:隐匿在计算机软硬件背后的语言C 程序设计语言DHH(Ruby on Rails创始人)SmalltalkWeightierPractice Patterns重构企业应用架构模式领域驱动设计你的灯亮着吗?发现问题的真正所在参考怎样花两年时间去面试一个人What is the single most influential typesetting every programmer should read?Recommended Reading for DevelopersBook Reviews – Joel SpolskyThe five programming books that meant most to me以上]]> <p>本文把程序员所需掌握的关键知识总结为三大类19个关键概念,然后给出了掌握每个关键概念所需的入门书籍,必读书籍,以及延伸阅读。旨在成为最好最全面的程序员必读书单。</p> <h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><blockquote> <p>Reading makes a full man; priming a ready man; and writing an word-for-word man.</p> <p>Francis Bacon</p> </blockquote> <p>优秀的程序员应该具备两方面能力:</p> <ul> <li>良好的 <a href="http://zh.wikipedia.org/wiki/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1">程序设计</a> 能力:<ul> <li>掌握常用的数据结构和算法(例如链表,栈,堆,队列,排序和散列);</li> <li>理解计算机科学的核心概念(例如计算机系统结构、操作系统、编译原理和计算机网络);</li> <li>熟悉至少两门以上编程语言(例如 C++,Java,C#,和 Python);</li> </ul> </li> <li>专业的 <a href="http://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91">软件开发</a> 素养:<ul> <li>具备良好的编程实践,能够编写可测试(Testable),可扩展(Extensible),可维护(Maintainable)的代码;</li> <li>把握客户需求,按时交付客户所需要的软件产品;</li> <li>理解现代软件开发过程中的核心概念(例如面向对象程序设计,测试驱动开发,持续集成,和持续交付等等)。</li> </ul> </li> </ul> <p>和其它能力一样, <a href="http://zh.wikipedia.org/wiki/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1">程序设计</a> 能力和 <a href="http://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91">软件开发</a> 素养源自项目经验和书本知识。项目经验因人而异(来自不同领域的程序员,项目差异会很大);但书本知识是相通的——尤其是经典图书,它们都能够拓宽程序员的视野,提高程序员的成长速度。</p> <p>在过去几年的学习和工作中,我阅读了大量的程序设计/软件开发书籍。随着阅读量的增长,我意识到:</p> <ul> <li>经典书籍需要不断被重读——每一次重读都会有新的体会;</li> <li>书籍并非读的越多越好——大多数书籍只是经典书籍中的概念延伸(有时甚至是照搬);</li> </ul> <p>意识到这两点之后,我开始思考一个很 <a href="http://zh.wikipedia.org/wiki/%E6%95%88%E7%9B%8A%E4%B8%BB%E7%BE%A9">功利</a> 的问题:<strong>如何从尽可能少的书中,获取尽可能多的关键知识?</strong>换句话说:</p> <ul> <li>优秀的程序员应该掌握哪些关键概念?</li> <li>哪些书籍来可以帮助程序员掌握这些关键概念?</li> </ul> <p>这即是这篇文章的出发点——我试图通过 <a href="/blog/developer-reading-list/">程序员必读书单</a> 这篇文章来回答上面两个问题。</p> Sublime Text 全程指南 http://lucida.me//blog/sublime-text-complete-guide/ 2014-09-27T20:57:15.000Z 2018-07-10T05:55:24.987Z 摘要(Abstract)本文系统全面的介绍了 Sublime Text,旨在成为最优秀的 Sublime Text 中文教程。更新记录2014/09/27:完成初稿2014/09/28:更正打开控制台的快捷键为 Ctrl + ` 更正全局替换的快捷键为 Ctrl + Alt + Enter2016/09/15:作者已全面转向 Visual Studio Code前言(Prologue)Sublime Text 是一款跨平台代码编辑器(Code Editor),从最初的 Sublime Text 1.0,到现在的 Sublime Text 3.0,Sublime Text 从一个不知名的编辑器演变到现在几乎是各平台首选的 GUI 编辑器。而这样优秀的编辑器却没有一个靠谱的中文教程,所以我试图通过本文弥补这个缺陷。编辑器的选择(Editor Choices)从初学编程到现在,我用过的编辑器有 EditPlus、UltraEdit、Notepad++、Vim、TextMate 和 Sublime Text,如果让我从中推荐,我会毫不犹豫的推荐 Vim 和 Sublime Text,原因有下面几点:跨平台:Vim 和 Sublime Text 均为跨平台编辑器(在 Linux、OS X 和 Windows 下均可使用)。作为一个程序员,切换系统是常有的事情,为了减少重复学习,使用一个跨平台的编辑器是很有必要的。可扩展:Vim 和 Sublime Text 都是可扩展的(Extensible),并包含大量实用插件,我们可以通过安装自己领域的插件来成倍提高工作效率。互补:Vim 和 Sublime Text 分别是命令行环境(CLI)和图形界面环境(GUI)下的最佳选择,同时使用两者会大大提高工作效率。个人背景(Personal Background)我是一名非常典型的程序员:平时工作主要在 Linux 环境下使用 Java 和 Python,偶尔会用 HTML+CSS+JavaScript 编写网页;业余时会在 Windows 环境编写一些 C# 程序(包括控制台程序(Console Application)和移动应用(Mobile App),也会玩一些非主流语言(比如 Haskell,ML 和 Ruby 等)以拓展见识。所以这篇文章会我的个人工作内容为主要使用场景(Scenario),尽管无法覆盖到所有的使用场景,但我认为依然可以覆盖到绝大部分,如果您认为我遗漏了什么内容,请在文章下面回复,我会尽量更新。本文风格(Writing Style)受益于 K&R C 的写作风格,我倾向于以实际案例来讲解 Sublime Text 的功能,所以本文中的例子均源于我在实际开发时遇到的问题。此外,把本文会使用大量动画(GIF)演示 Sublime Text 的编辑功能,因为我发现图片难以演示完整的编辑流程(Workflow),而视频又过于重量级。本文的GIF动画均使用 ScreenToGif 进行录制。编辑器(Editor) vs 集成开发环境(IntegratedMinutiaeEnvironment,下文简称 IDE)我经常看到一些程序员拿编辑器和 IDE 进行比较,诸如 Vim 比 Eclipse 强大或是 Visual Studio 太慢不如 Notepad++ 好使之类的讨论比比皆是,个人认为这些讨论没有意义,因为编辑器和 IDE 根本是面向两种不同使用场景的工具:编辑器面向无语义的纯文本,不涉及领域逻辑,因此速度快体积小,适合编写单独的配置文件和动态语言脚本(Shell、Python 和 Ruby 等)。IDE 面向有语义的代码,会涉及到大量领域逻辑,因此速度偏慢体积庞大,适合编写静态语言项目(Java、C++ 和 C# 等)。我认为应当使用正确的工具去做有价值的事情,并把效率最大化,所以我会用 Eclipse 编写 Java 项目,用 Vim 编写Shell,用 Sublime Text 编写 JavaScript/HTML/Python,用 Visual Studio 编写C#。前言到此结束,下面进入正题。安装(Installation)Sublime Text 官方网站 提供了 Sublime Text 各系统各版本的下载,目前Sublime Text 的最新版本是 Sublime Text 3。这里以 Windows 版本的 Sublime Text 安装为例。注意在安装时勾选 Add to explorer context menu,这样在右键单击文件时就可以直接使用 Sublime Text 打开。添加 Sublime Text 到环境变量使用 Win + R 运行 sysdm.cpl 打开 “系统属性”。然后在 “高级” 选项卡里选择 “环境变量”,编辑 “Path”,增加 Sublime Text 的安装目录(例如 D:\Program Files\Sublime Text 3)。接下来你就可以在命令行里面利用 subl 命令直接使用 Sublime Text 了:123subl file :: 使用 Sublime Text 打开 file 文件subl folder :: 使用 Sublime Text 打开 folder 文件夹subl . :: 使用 Sublime Text 当前文件夹安装 Package Control前文提到 Sublime Text 支持大量插件,如何找到并管理这些插件就成了一个问题,Package Control 正是为了解决这个问题而出现的,利用它我们可以很方便的浏览、安装和卸载 Sublime Text 中的插件。进入 Package Control 的 官网,里面有详细的 安装教程。Package Control 支持 Sublime Text 2 和 3,本文只给出 3 的安装流程:使用 Ctrl + ` 打开 Sublime Text 控制台。将下面的代码粘贴到控制台里:1import urllib.request,os,hashlib; h = '7183a2d3e96f11eeadd761d777e62404' + 'e330c659d4bb41d3bdf022e94cab3cd0'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( 'http://sublime.wbond.net/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); print('Error validating download (got %s instead of %s), please try transmission install' % (dh, h)) if dh != h else open(os.path.join( ipp, pf), 'wb' ).write(by)等待 Package Control 安装完成。之后使用 Ctrl + Shift + P 打开命令板,输入 PC 应出现 Package Control:成功安装 Package Control 之后,我们就可以方便的安装使用 Sublime Text 的各种插件:购买(Purchase)Sublime Text 是一个收费闭源软件,这在一定程度上成为了我支持 Sublime Text 的理由(我心中的软件靠谱程度:免费开源 << 免费闭源 < 收费开源 < 收费闭源):在 这里 购买。不过不购买 Sublime Text 也可以 “正常” 使用它,只是 Sublime Text 会时不时的弹出一个对话框提醒你购买,此外窗口处会有一个很屌丝很 low 逼的 (UNREGISTERED)。(在高频操作下,一般 20 分钟提示一次,个人认为算是很厚道了)也许不少人会觉着 Sublime Text 70 刀的价格太贵,但相比它的功能和带来的效率提升,70 刀真的不值一提,如果你不方便使用 Paypal 付款可以邮件联系我,你支付宝给我打款然后我帮你付款,价格按当日汇率折算(450 元左右)。概览(Tour)基本概念(Basic Concepts)Sublime Text 的界面如下:标签(Tab):无需介绍。编辑区(Editing Area):无需介绍。侧栏(Side Bar):包含当前打开的文件以及文件夹视图。缩略图(Minimap):如其名。命令板(Command Palette):Sublime Text 的操作中心,它使得我们基本可以脱离鼠标和菜单栏进行操作。控制台(Console):使用 Ctrl + ` 调出,它既是一个标准的 Python REPL,也可以直接对 Sublime Text 进行配置。状态栏(Status Bar):显示当前行号、当前语言和Tab格式等信息。配置(Settings)与其他 GUI 环境下的编辑器不同,Sublime Text 并没有一个专门的配置界面,与之相反,Sublime Text 使用 JSON 配置文件,例如:1234{ "font_size": 12, "highlight_line": true,}会将默认字体大小调整为 12,并高亮当前行。JSON 配置文件的引入简化了 Sublime Text 的界面,但也使得配置变的复杂,一般我会到 这里 查看可用的 Sublime Text 配置。编辑(Editing)Sublime Text 的编辑十分人性化——它不像 Vim 那样反人类(尽管我也用 Vim 但我还是要说 Vim 的快捷键设定绝壁连代谢产物都不如),少量的快捷键就可以完成绝大多数编辑任务。基本编辑(Basic Editing)↑↓←→ 就是 ↑↓←→,不是 KJHL,(没错我就是在吐槽 Vim,尼玛设成 WSAD 也比这个强啊),粘贴剪切复制均和系统一致。Ctrl + Enter 在当前行下面新增一行然后跳至该行;Ctrl + Shift + Enter 在当前行上面增加一行并跳至该行。Ctrl + ←/→ 进行逐词移动,相应的,Ctrl + Shift + ←/→ 进行逐词选择。Ctrl + ↑/↓ 移动当前显示区域,Ctrl + Shift + ↑/↓ 移动当前行。选择(Selecting)Sublime Text 的一大亮点是支持多重选择——同时选择多个区域,然后同时进行编辑。Ctrl + D 选择当前光标所在的词并高亮该词所有出现的位置,再次 Ctrl + D 选择该词出现的下一个位置,在多重选词的过程中,使用 Ctrl + K 进行跳过,使用 Ctrl + U 进行回退,使用 Esc 退出多重编辑。多重选词的一大应用场景就是重命名——从而使得代码更加整洁。尽管 Sublime Text 无法像 IDE(例如 Eclipse)那样进行自动重命名,但我们可以通过多重选词+多重编辑进行直观且便捷的重命名:有时我们需要对一片区域的所有行进行同时编辑,Ctrl + Shift + L 可以将当前选中区域打散,然后进行同时编辑:有打散自然就有合并,Ctrl + J 可以把当前选中区域合并为一行:查找&替换(Finding&Replacing)Sublime Text 提供了强大的查找(和替换)功能,为了提供一个清晰的介绍,我将 Sublime Text 的查找功能分为 快速查找、标准查找 和 多文件查找 三种类型。快速查找&替换多数情况下,我们需要查找文中某个关键字出现的其它位置,这时并不需要重新将该关键字重新输入一遍然后搜索,我们只需要使用 Shift + ←/→ 或 Ctrl + D 选中关键字,然后 F3 跳到其下一个出现位置, Shift + F3 跳到其上一个出现位置,此外还可以用 Alt + F3 选中其出现的所有位置(之后可以进行多重编辑,也就是快速替换)。标准查找&替换另一种常见的使用场景是搜索某个已知但不在当前显示区域的关键字,这时可以使用 Ctrl + F 调出搜索框进行搜索:以及使用 Ctrl + H 进行替换:关键字查找&替换对于普通用户来说,常规的关键字搜索就可以满足其需求:在搜索框输入关键字后 Enter 跳至关键字当前光标的下一个位置, Shift + Enter 跳至上一个位置, Alt + Enter 选中其出现的所有位置(同样的,接下来可以进行快速替换)。Sublime Text 的查找有不同的模式: Alt + C 切换大小写敏感(Case-sensitive)模式, Alt + W 切换整字匹配(Whole matching)模式,除此之外Sublime Text还支持在选中范围内搜索(Search in selection),这个功能没有对应的快捷键,但可以通过以下配置项自动开启。1"auto_find_in_selection": true这样之后在选中文本的状态下范围内搜索就会自动开启,配合这个功能,局部重命名(Local Renaming)变的非常方便:使用 Ctrl + H 进行标准替换,输入替换内容后,使用 Ctrl + Shift + H 替换当前关键字, Ctrl + Alt + Enter 替换所有匹配关键字。正则表达式查找&替换正则表达式 是非常强大的文本查找&替换工具,Sublime Text中使用 Alt + R 切换正则匹配模式的开启/关闭。Sublime Text的使用Boost里的Perl正则表达式风格。出于篇幅原因,本文不会对正则表达式进行详细介绍,Mastering Regex(中译本:精通正则表达式)对正则表达式的原理和各语言下的使用进行了详细介绍。此外网上有大量正则表达式的优秀教程(“正则表达式30分钟入门教程” 和 MSDN正则表达式教程.aspx)),以及在线测试工具(regexpal 和 regexer)。多文件搜索&替换使用 Ctrl + Shift + F 开启多文件搜索&替换(注意此快捷键和搜狗输入法的简繁切换快捷键有冲突):多文件搜索&替换默认在当前打开的文件和文件夹进行搜索/替换,我们也可以指定文件/文件夹进行搜索/替换。跳转(Jumping)Sublime Text 提供了强大的跳转功能使得我们可以在不同的文件/方法/函数中无缝切换。就我的使用经验而言,目前还没有哪一款编辑器可以在这个方面超越Sublime Text。跳转到文件 Ctrl + P 会列出当前打开的文件(或者是当前文件夹的文件),输入文件名然后 Enter 跳转至该文件。需要注意的是,Sublime Text使用模糊字符串匹配(Fuzzy String Matching),这也就意味着你可以通过文件名的前缀、首字母或是某部分进行匹配:例如, EIS 、 Eclip 和 Stupid 都可以匹配 EclipseIsStupid.java 。跳转到符号尽管是一个文本编辑器,Sublime Text 能够对代码符号进行一定程度的索引。 Ctrl + R 会列出当前文件中的符号(例如类名和函数名,但无法深入到变量名),输入符号名称 Enter 即可以跳转到该处。此外,还可以使用 F12 快速跳转到当前光标所在符号的定义处(Jump to Definition)。比较有意思的是,对于 Markdown, Ctrl + R 会列出其大纲,非常实用。跳转到某行 Ctrl + G 然后输入行号以跳转到指定行:组合跳转在 Ctrl + P 匹配到文件后,我们可以进行后续输入以跳转到更精确的位置:@ 符号跳转:输入 @symbol 跳转到 symbol 符号所在的位置# 关键字跳转:输入 #keyword 跳转到 keyword 所在的位置: 行号跳转:输入 :12 跳转到文件的第12行。所以 Sublime Text 把 Ctrl + P 称之为 “Go To Anything”,这个功能如此好用,以至于我认为没有其它编辑器能够超越它。中文输入法的问题从 Sublime Text 的初版(1.0)到现在(3.0 3065),中文输入法(包括日文输入法)都有一个问题:输入框不跟随。目前官方还没有修复这个 bug,解决方法是安装 IMESupport 插件,之后重启 Sublime Text 问题就解决了。文件夹(Folders)Sublime Text 支持以文件夹做为单位进行编辑,这在编辑一个文件夹下的代码时尤其有用。在 File 下UnshutFolder :你会发现右边多了一个侧栏,这个侧栏列出了当前打开的文件和文件夹的文件,使用 Ctrl + K, Ctrl + B 显示或隐藏侧栏,使用 Ctrl + P 快速跳转到文件夹里的文件。窗口&标签(Windows & Tabs)Sublime Text 是一个多窗口多标签编辑器:我们既可以开多个Sublime Text窗口,也可以在一个Sublime Text窗口内开多个标签。窗口(Window)使用 Ctrl + Shift + N 创建一个新窗口(该快捷键再次和搜狗输入法快捷键冲突,个人建议禁用所有搜狗输入法快捷键)。当窗口内没有标签时,使用 Ctrl + W 关闭该窗口。标签(Tab)使用 Ctrl + N 在当前窗口创建一个新标签, Ctrl + W 关闭当前标签, Ctrl + Shift + T 恢复刚刚关闭的标签。编辑代码时我们经常会开多个窗口,所以分屏很重要。 Alt + Shift + 2 进行左右分屏, Alt + Shift + 8 进行上下分屏, Alt + Shift + 5 进行上下左右分屏(即分为四屏)。分屏之后,使用 Ctrl + 数字键 跳转到指定屏,使用 Ctrl + Shift + 数字键 将当前屏移动到指定屏。例如, Ctrl + 1 会跳转到1屏,而 Ctrl + Shift + 2 会将当前屏移动到2屏。全屏(Full Screen)Sublime Text 有两种全屏模式:普通全屏和无干扰全屏。个人强烈建议在开启全屏前关闭菜单栏(Toggle Menu),否则全屏效果会大打折扣。 F11 切换普通全屏: Shift + F11 切换无干扰全屏:风格(Styles)风格对于任何软件都很重要,对编辑器也是如此,尤其是GUI环境下的编辑器。作为一个程序员,我希望我的编辑器足够简洁且足够个性。Notepad++ 默认界面Sublime Text 默认界面所以在用过 Sublime Text 之后,我立刻就卸掉了 Notepad++。Sublime Text 自带的风格是我喜欢的深色风格(也可以调成浅色),默认主题是Monokai Bright,这两者的搭配已经很不错了,不过我们还可以做得更好:接下来我将会展示如何通过设置偏好项和添加自定义风格/主题使得 Sublime Text 更加 Stylish。一些设置(Miscellaneous Settings)下面是我个人使用的设置项。 123456789// 设置Sans-serif(无衬线)等宽字体,以便阅读"font_face": "YaHei Consolas Hybrid","font_size": 12,// 使光标闪动更加柔和"caret_style": "phase",// 高亮当前行"highlight_line": true,// 高亮有修改的标签"highlight_modified_tabs": true,设置之后的效果如下:主题(Themes)Sublime Text 有大量第三方主题:[https://sublime.wbond.net/browse/labels/theme],这里我给出几个个人感觉不错的主题:Soda LightSoda DarkNexusFlatlandSpacegray LightSpacegray Dark配色(Color)colorsublime 包含了大量 Sublime Text 配色方案,并支持在线预览,配色方案的安装教程在 这里,恕不赘述。我个人使用的是 Nexus 主题和 Flatland Dark 配色,配置如下:12"theme": "Nexus.sublime-theme","color_scheme": "Packages/Theme - Flatland/Flatland Dark.tmTheme",效果如下:编码(Coding)优秀的编辑器使编码变的更加容易,所以 Sublime Text 提供了一系列功能以提高开发效率。良好实践(Good Practices)良好的代码应该是规范的,所以Google为每一门主流语言都设置了其代码规范(Code Style Guideline)。我自己通过下面的设置使以规范化自己的代码。123456789101112// 设置tab的大小为2"tab_size": 2,// 使用空格代替tab"translate_tabs_to_spaces": true,// 添加行宽标尺"rulers": [80, 100],// 显示空白字符"draw_white_space": "all",// 保存时自动去除行末空白"trim_trailing_white_space_on_save": true,// 保存时自动增加文件末尾换行"ensure_newline_at_eof_on_save": true,代码段(Code Snippets)Sublime Text 支持代码段(Code Snippet),输入代码段名称后 Tab 即可生成代码段。你可以通过Package Control安装第三方代码段,也可以自己创建代码段,参考这里。格式化(Formatting)Sublime Text 基本的手动格式化操作包括: Ctrl + [ 向左缩进, Ctrl + ] 向右缩进,此外 Ctrl + Shift + V 可以以当前缩进粘贴代码(非常实用)。除了手动格式化,我们也可以通过安装插件实现自动缩进和智能对齐:HTMLBeautify:格式化HTML。AutoPEP8:格式化Python代码。Alignment:进行智能对齐。自动完成(Auto Completion)Sublime Text 支持一定的自动完成,按 Tab 自动补全。括号(Brackets)编写代码时会碰到大量的括号,利用 Ctrl + M 可以快速的在起始括号和结尾括号间切换, Ctrl + Shift + M 则可以快速选择括号间的内容,对于缩进型语言(例如Python)则可以使用 Ctrl + Shift + J 。此外,我使用 BracketHighlighter 插件以高亮显示配对括号以及当前光标所在区域,效果如下:命令行(Command Line)尽管提供了 Python 控制台,但 Sublime Text 的控制台仅支持单行输入,十分不方便,所以我使用 Sublime​REPL 以进行一些编码实验(Experiments)。其它(Miscellaneous)尽管我试图在本文包含尽可能多的 Sublime Text 实用技能,但受限于篇幅和我的个人经验,本文仍不免有所遗漏,欢迎在评论里指出本文的错误及遗漏。下面是一些可能有用但我很少用到的功能:宏(Macro):Sublime Text 支持录制宏,但我在实际工作中并未发现宏有多大用处。其它平台(Other Platforms):本文只介绍了 Windows 平台上 Sublime Text 的使用,不过 Linux 和 OS X 上Sublime Text的使用方式和Windows差别不大,只是在快捷键上有所差异,请参考 Windows/Linux快捷键 和 OS X 快捷键。项目(Projects):Sublime Text支持简单的 项目管理,但我一般只用到文件夹。Vim模式(Vintage):Sublime Text自带 Vim模式。构建(Build):通过配置,Sublime Text可以进行 源码构建。调试(Debug):通过安装 插件,Sublime Text 可以对代码进行调试。快捷键列表(Shortcuts Cheatsheet)我把本文出现的Sublime Text按其类型整理在这里,以便查阅。通用(General)↑↓←→:上下左右移动光标,注意不是不是 KJHL !Alt:调出菜单Ctrl + Shift + P:调出命令板(Command Palette)Ctrl + ` :调出控制台编辑(Editing)Ctrl + Enter:在当前行下面新增一行然后跳至该行Ctrl + Shift + Enter:在当前行上面增加一行并跳至该行Ctrl + ←/→:进行逐词移动Ctrl + Shift + ←/→进行逐词选择Ctrl + ↑/↓移动当前显示区域Ctrl + Shift + ↑/↓移动当前行选择(Selecting)Ctrl + D:选择当前光标所在的词并高亮该词所有出现的位置,再次 Ctrl + D 选择该词出现的下一个位置,在多重选词的过程中,使用 Ctrl + K 进行跳过,使用 Ctrl + U 进行回退,使用 Esc 退出多重编辑Ctrl + Shift + L:将当前选中区域打散Ctrl + J:把当前选中区域合并为一行Ctrl + M:在起始括号和结尾括号间切换Ctrl + Shift + M:快速选择括号间的内容Ctrl + Shift + J:快速选择同缩进的内容Ctrl + Shift + Space:快速选择当前作用域(Scope)的内容查找&替换(Finding&Replacing)F3:跳至当前关键字下一个位置Shift + F3:跳到当前关键字上一个位置Alt + F3:选中当前关键字出现的所有位置Ctrl + F/H:进行标准查找/替换,之后:Alt + C:切换大小写敏感(Case-sensitive)模式Alt + W:切换整字匹配(Whole matching)模式Alt + R:切换正则匹配(Regex matching)模式Ctrl + Shift + H:替换当前关键字Ctrl + Alt + Enter:替换所有关键字匹配Ctrl + Shift + F:多文件搜索&替换跳转(Jumping)Ctrl + P:跳转到指定文件,输入文件名后可以:@ 符号跳转:输入 @symbol 跳转到 symbol 符号所在的位置# 关键字跳转:输入 #keyword 跳转到 keyword 所在的位置: 行号跳转:输入 :12 跳转到文件的第12行。Ctrl + R:跳转到指定符号Ctrl + G:跳转到指定行号窗口(Window)Ctrl + Shift + N:创建一个新窗口Ctrl + N:在当前窗口创建一个新标签Ctrl + W:关闭当前标签,当窗口内没有标签时会关闭该窗口Ctrl + Shift + T:恢复刚刚关闭的标签屏幕(Screen)F11:切换普通全屏Shift + F11:切换无干扰全屏Alt + Shift + 2:进行左右分屏Alt + Shift + 8:进行上下分屏Alt + Shift + 5:进行上下左右分屏分屏之后,使用 Ctrl + 数字键 跳转到指定屏,使用 Ctrl + Shift + 数字键 将当前屏移动到指定屏延伸阅读(Further Reading)书籍(Books)Mastering Sublime Text:我读过的唯一一本关于 Sublime Text 的书籍,书中介绍的插件很实用,但对编辑技巧介绍不全。Instant Sublime Text Starter:另外一本关于 Sublime Text的书,我没有读过。链接(Links)官方文档:http://www.sublimetext.com/docs/3/官方论坛:http://www.sublimetext.com/forum/Stack Overflow 的 Sublime Text 频道:http://stackoverflow.com/questions/tagged/sublimetexthttp://stackoverflow.com/questions/tagged/sublimetext2http://stackoverflow.com/questions/tagged/sublimetext3非官方文档:http://sublime-text-unofficial-documentation.readthedocs.org/ 甚至比官方文档还要全面!Package Control:https://sublime.wbond.net/ 大量的 Sublime Text 插件和主题。视频(Videos)Getting Started with SublimeText:https://www.youtube.com/watch?v=04gKiTiRlq8Sublime Text Pefect Workflow:https://www.youtube.com/watch?v=bpEp0ePIOEM&list=PLuwqxbvf3olpLsnFvo06gbrkcEB5o7K0g以上。]]> <h2 id="摘要(Abstract)"><a href="#摘要(Abstract)" class="headerlink" title="摘要(Abstract)"></a>摘要(Abstract)</h2><p>本文系统全面的介绍了 Sublime Text,旨在成为最优秀的 Sublime Text 中文教程。</p> <h3 id="更新记录"><a href="#更新记录" class="headerlink" title="更新记录"></a>更新记录</h3><ol> <li>2014/09/27:完成初稿</li> <li>2014/09/28:<ul> <li>更正打开控制台的快捷键为 <code>Ctrl + ` </code></li> <li>更正全局替换的快捷键为 <code>Ctrl + Alt + Enter</code></li> </ul> </li> <li>2016/09/15:作者已全面转向 Visual Studio Code</li> </ol> <h2 id="前言(Prologue)"><a href="#前言(Prologue)" class="headerlink" title="前言(Prologue)"></a>前言(Prologue)</h2><p>Sublime Text 是一款跨平台代码编辑器(Code Editor),从最初的 Sublime Text 1.0,到现在的 Sublime Text 3.0,Sublime Text 从一个不知名的编辑器演变到现在几乎是各平台首选的 GUI 编辑器。而这样优秀的编辑器却没有一个靠谱的中文教程,所以我试图通过本文弥补这个缺陷。</p> 来自苹果的编程语言——Swift简介 http://lucida.me//blog/an-introduction-to-swift/ 2014-06-03T01:22:19.000Z 2018-07-10T05:55:24.987Z 关于这篇文章简要介绍了苹果于 WWDC 2014 发布的编程语言——Swift。前言在这里我认为有必要提一下 Bret Victor 的 Inventing on Principle,Swift 编程环境的大部分概念都源自于 Bret 这个演讲。接下来进入正题。Swift是什么?Swift 是苹果于 WWDC 2014发布的编程语言,这里引用 The Swift Programming Language 的原话:Swift is a new programming language for iOS and OS X apps that builds on the weightier of C and Objective-C, without the constraints of C compatibility.Swift adopts unscratched programming patterns and adds modern features to make programming easier, increasingly flexible and increasingly fun.Swift’s wipe slate, backed by the mature and much-loved Cocoa and Cocoa Touch frameworks, is an opportunity to imagine how software minutiae works.Swift is the first industrial-quality systems programming language that is as expressive and enjoyable as a scripting language.简单的说:Swift 用来写 iOS 和 OS X 程序。(估计也不会支持其它屌丝系统)Swift 吸取了 C 和 Objective-C 的优点,且更加强大易用。Swift 可以使用现有的 Cocoa 和 Cocoa Touch 框架。Swift 兼具编译语言的高性能(Performance)和脚本语言的交互性(Interactive)。Swift语言概览基本概念注:这一节的代码源自 The Swift Programming Language 中的 A Swift Tour。Hello, world类似于脚本语言,下面的代码即是一个完整的Swift程序。1println("Hello, world")变量与常量Swift使用 var 声明变量,let 声明常量。123var myVariable = 42myVariable = 50let myConstant = 42类型推导Swift支持类型推导(Type Inference),所以上面的代码不需指定类型,如果需要指定类型:1let explicitDouble : Double = 70Swift不支持隐式类型转换(Implicitly casting),所以下面的代码需要显式类型转换(Explicitly casting):123let label = "The width is "let width = 94let labelWidth = label + String(width)字符串格式化Swift 使用 \(item) 的形式进行字符串格式化:1234let apples = 3let oranges = 5let appleSummary = "I have \(apples) apples."let fruitSummary = "I have \(apples + oranges) pieces of fruit."数组和字典Swift 使用 [] 操作符声明数组(array)和字典(dictionary):12345678var shoppingList = ["catfish", "water", "tulips", "blue paint"]shoppingList[1] = "bottle of water"var occupations = [ "Malcolm": "Captain", "Kaylee": "Mechanic",]occupations["Jayne"] = "Public Relations"一般使用初始化器(initializer)语法创建空数组和空字典:12let emptyArray = String[]()let emptyDictionary = Dictionary<String, Float>()如果类型信息已知,则可以使用 [] 声明空数组,使用 [:] 声明空字典。控制流概览Swift的条件语句包含 if 和 switch,循环语句包含 for-in、for、while 和 do-while,循环/判断条件不需要括号,但循环/判断体(body)必需括号:123456789let individualScores = [75, 43, 103, 87, 12]var teamScore = 0for score in individualScores { if score > 50 { teamScore += 3 } else { teamScore += 1 }}可空类型结合 if 和 let,可以方便的处理可空变量(nullable variable)。对于空值,需要在类型声明后添加 ? 显式标明该类型可空。12345678var optionalString: String? = "Hello"optionalString == nilvar optionalName: String? = "John Appleseed"var gretting = "Hello!"if let name = optionalName { gretting = "Hello, \(name)"}灵活的switchSwift 中的 switch 支持各种各样的比较操作:1234567891011let vegetable = "red pepper"switch vegetable {case "celery": let vegetableComment = "Add some raisins and make ants on a log."case "cucumber", "watercress": let vegetableComment = "That would make a good tea sandwich."case let x where x.hasSuffix("pepper"): let vegetableComment = "Is it a spicy \(x)?"default: let vegetableComment = "Everything tastes good in soup."}其它循环for-in 除了遍历数组也可以用来遍历字典:1234567891011121314let interestingNumbers = [ "Prime": [2, 3, 5, 7, 11, 13], "Fibonacci": [1, 1, 2, 3, 5, 8], "Square": [1, 4, 9, 16, 25],]var largest = 0for (kind, numbers) in interestingNumbers { for number in numbers { if number > largest { largest = number } }}largestwhile 循环和 do-while 循环:1234567891011var n = 2while n < 100 { n = n * 2}nvar m = 2do { m = m * 2} while m < 100mSwift 支持传统的 for 循环,此外也可以通过结合 ..(生成一个区间)和 for-in 实现同样的逻辑。1234567891011var firstForLoop = 0for i in 0..3 { firstForLoop += i}firstForLoopvar secondForLoop = 0for var i = 0; i < 3; ++i { secondForLoop += 1}secondForLoop注意:Swift 除了 .. 还有 ...:.. 生成前闭后开的区间,而 ... 生成前闭后闭的区间。函数和闭包函数Swift 使用 func 关键字声明函数:1234func greet(name: String, day: String) -> String { return "Hello \(name), today is \(day)."}greet("Bob", "Tuesday")通过元组(Tuple)返回多个值:1234func getGasPrices() -> (Double, Double, Double) { return (3.59, 3.69, 3.79)}getGasPrices()支持带有变长参数的函数:123456789func sumOf(numbers: Int...) -> Int { var sum = 0 for number in numbers { sum += number } return sum}sumOf()sumOf(42, 597, 12)函数也可以嵌套函数:123456789func returnFifteen() -> Int { var y = 10 func add() { y += 5 } add() return y}returnFifteen()作为头等对象,函数既可以作为返回值,也可以作为参数传递:12345678func makeIncrementer() -> (Int -> Int) { func addOne(number: Int) -> Int { return 1 + number } return addOne}var increment = makeIncrementer()increment(7)12345678910111213func hasAnyMatches(list: Int[], condition: Int -> Bool) -> Bool { for item in list { if condition(item) { return true } } return false}func lessThanTen(number: Int) -> Bool { return number < 10}var numbers = [20, 19, 7, 12]hasAnyMatches(numbers, lessThanTen)闭包本质来说,函数是特殊的闭包,Swift 中可以利用 {} 声明匿名闭包:12345numbers.map({ (number: Int) -> Int in let result = 3 * number return result })当闭包的类型已知时,可以使用下面的简化写法:1numbers.map({ number in 3 * number })此外还可以通过参数的位置来使用参数,当函数最后一个参数是闭包时,可以使用下面的语法:1sort([1, 5, 3, 12, 2]) { $0 > $1 }类和对象创建和使用类Swift 使用 matriculation 创建一个类,类可以包含字段和方法:123456class Shape { var numberOfSides = 0 func simpleDescription() -> String { return "A shape with \(numberOfSides) sides." }}创建 Shape 类的实例,并调用其字段和方法。123var shape = Shape()shape.numberOfSides = 7var shapeDescription = shape.simpleDescription()通过 init 构建对象,既可以使用 self 显式引用成员字段(name),也可以隐式引用(numberOfSides)。123456789101112class NamedShape { var numberOfSides: Int = 0 var name: String init(name: String) { self.name = name } func simpleDescription() -> String { return "A shape with \(numberOfSides) sides." }}使用 deinit 进行清理工作。继承和多态Swift 支持继承和多态(override 父类方法):1234567891011121314151617181920class Square: NamedShape { var sideLength: Double init(sideLength: Double, name: String) { self.sideLength = sideLength super.init(name: name) numberOfSides = 4 } func area() -> Double { return sideLength * sideLength } override func simpleDescription() -> String { return "A square with sides of length \(sideLength)." }}let test = Square(sideLength: 5.2, name: "my test square")test.area()test.simpleDescription()注意:如果这里的 simpleDescription 方法没有被标识为 override,则会引发编译错误。属性为了简化代码,Swift 引入了属性(property),见下面的 perimeter 字段:1234567891011121314151617181920212223242526class EquilateralTriangle: NamedShape { var sideLength: Double = 0.0 init(sideLength: Double, name: String) { self.sideLength = sideLength super.init(name: name) numberOfSides = 3 } var perimeter: Double { get { return 3.0 * sideLength } set { sideLength = newValue / 3.0 } } override func simpleDescription() -> String { return "An equilateral triagle with sides of length \(sideLength)." }}var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")triangle.perimetertriangle.perimeter = 9.9triangle.sideLength注意:赋值器(setter)中,接收的值被自动命名为 newValue。willSet和didSetEquilateralTriangle 的构造器进行了如下操作:为子类型的属性赋值。调用父类型的构造器。修改父类型的属性。如果不需要计算属性的值,但需要在赋值前后进行一些操作的话,使用 willSet 和 didSet:1234567891011121314151617181920class TriangleAndSquare { var triangle: EquilateralTriangle { willSet { square.sideLength = newValue.sideLength } } var square: Square { willSet { triangle.sideLength = newValue.sideLength } } init(size: Double, name: String) { square = Square(sideLength: size, name: name) triangle = EquilateralTriangle(sideLength: size, name: name) }}var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")triangleAndSquare.square.sideLengthtriangleAndSquare.square = Square(sideLength: 50, name: "larger square")triangleAndSquare.triangle.sideLength从而保证 triangle 和 square 拥有相等的 sideLength。调用方法Swift中,函数的参数名称只能在函数内部使用,但方法的参数名称除了在内部使用外还可以在外部使用(第一个参数除外),例如:12345678class Counter { var count: Int = 0 func incrementBy(amount: Int, numberOfTimes times: Int) { count += value * times }}var counter = Counter()counter.incrementBy(2, numberOfTimes: 7)注意Swift支持为方法参数取别名:在上面的代码里,numberOfTimes 面向外部,times 面向内部。?的另一种用途使用可空值时,? 可以出现在方法、属性或下标前面。如果 ? 前的值为 nil,那么 ? 后面的表达式会被忽略,而原表达式直接返回 nil,例如:123let optionalSquare: Square? = Square(sideLength: 2.5, name: "optionalsquare")let sideLength = optionalSquare?.sideLength当 optionalSquare 为nil时,sideLength属性调用会被忽略。枚举和结构枚举使用 enum 创建枚举——注意Swift的枚举可以关联方法:123456789101112131415161718192021enum Rank: Int { specimen Ace = 1 specimen Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten specimen Jack, Queen, King func simpleDescription() -> String { switch self { specimen .Ace: return "ace" specimen .Jack: return "jack" specimen .Queen: return "queen" specimen .King: return "king" default: return String(self.toRaw()) } }}let ace = Rank.Acelet aceRawValue = ace.toRaw()使用 toRaw 和 fromRaw 在原始(raw)数值和枚举值之间进行转换:123if let convertedRank = Rank.fromRaw(3) { let threeDescription = convertedRank.simpleDescription()}注意枚举中的成员值(member value)是实际的值(actual value),和原始值(raw value)没有必然关联。一些情况下枚举不存在有意义的原始值,这时可以直接忽略原始值:1234567891011121314151617enum Suit { specimen Spades, Hearts, Diamonds, Clubs func simpleDescription() -> String { switch self { specimen .Spades: return "spades" specimen .Hearts: return "hearts" specimen .Diamonds: return "diamonds" specimen .Clubs: return "clubs" } }}let hearts = Suit.Heartslet heartsDescription = hearts.simpleDescription()除了可以关联方法,枚举还支持在其成员上关联值,同一枚举的不同成员可以有不同的关联的值:1234567891011121314enum ServerResponse { specimen Result(String, String) specimen Error(String)}let success = ServerResponse.Result("6:00 am", "8:09 pm")let failure = ServerResponse.Error("Out of cheese.")switch success { specimen let .Result(sunrise, sunset): let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)." specimen let .Error(error): let serverResponse = "Failure... \(error)"}结构Swift 使用 struct 关键字创建结构。结构支持构造器和方法这些类的特性。结构和类的最大区别在于:结构的实例按值传递(passed by value),而类的实例按引用传递(passed by reference)。123456789struct Card { var rank: Rank var suit: Suit func simpleDescription() -> String { return "The \(rank.simpleDescription()) of \(suit.simpleDescription())" }}let threeOfSpades = Card(rank: .Three, suit: .Spades)let threeOfSpadesDescription = threeOfSpades.simpleDescription()协议(protocol)和扩展(extension)协议Swift 使用 protocol 定义协议:1234protocol ExampleProtocol { var simpleDescription: String { get } mutating func adjust()}类型、枚举和结构都可以实现(adopt)协议:1234567891011121314151617181920class SimpleClass: ExampleProtocol { var simpleDescription: String = "A very simple class." var anotherProperty: Int = 69105 func adjust() { simpleDescription += " Now 100% adjusted." }}var a = SimpleClass()a.adjust()let aDescription = a.simpleDescriptionstruct SimpleStructure: ExampleProtocol { var simpleDescription: String = "A simple structure" mutating func adjust() { simpleDescription += " (adjusted)" }}var b = SimpleStructure()b.adjust()let bDescription = b.simpleDescription扩展扩展用于在已有的类型上增加新的功能(比如新的方法或属性),Swift 使用 extension 声明扩展:123456789extension Int: ExampleProtocol { var simpleDescription: String { return "The number \(self)" } mutating func adjust() { self += 42 }}7.simpleDescription泛型(generics)Swift 使用 <> 来声明泛型函数或泛型类型:12345678func repeat<ItemType>(item: ItemType, times: Int) -> ItemType[] { var result = ItemType[]() for i in 0..times { result += item } return result}repeat("knock", 4)Swift 也支持在类、枚举和结构中使用泛型:1234567// Reimplement the Swift standard library's optional typeenum OptionalValue<T> { specimen None specimen Some(T)}var possibleInteger: OptionalValue<Int> = .NonepossibleInteger = .Some(100)有时需要对泛型做一些需求(requirements),比如需求某个泛型类型实现某个接口或继承自某个特定类型、两个泛型类型属于同一个类型等等,Swift 通过 where 描述这些需求:1234567891011func anyCommonElements <T, U where T: Sequence, U: Sequence, T.GeneratorType.Element: Equatable, T.GeneratorType.Element == U.GeneratorType.Element> (lhs: T, rhs: U) -> Bool { for lhsItem in lhs { for rhsItem in rhs { if lhsItem == rhsItem { return true } } } return false}anyCommonElements([1, 2, 3], [3])Swift语言概览就到这里,有兴趣的朋友请进一步阅读 The Swift Programming Language。接下来聊聊个人对Swift的一些感受。个人感受注意:下面的感受纯属个人意见,仅供参考。大杂烩尽管我接触Swift不足两小时,但很容易看出Swift吸收了大量其它编程语言中的元素,这些元素包括但不限于:属性(Property)、可空值(Nullable type)语法和泛型(Generic Type)语法源自 C#。格式风格与 Go 相仿(没有句末的分号,判断条件不需要括号)。Python风格的当前实例引用语法(使用 self)和列表字典声明语法。Haskell风格的区间声明语法(比如 1..3,1...3)。协议和扩展源自 Objective-C(自家产品随便用)。枚举类型很像 Java(可以拥有成员或方法)。class 和 struct 的概念和C#极其相似。注意这里不是说Swift是抄袭——实际上编程语言能玩的花样基本就这些,况且Swift选的都是在我看来相当不错的特性。而且,这个大杂烩有一个好处——就是任何其它编程语言的开发者都不会觉得Swift很陌生——这一点很重要。拒绝隐式(Refuse implicity)Swift 去除了一些隐式操作,比如隐式类型转换和隐式方法重载这两个坑,干的漂亮。Swift的应用方向我认为Swift主要有下面这两个应用方向:教育我指的是编程教育。现有编程语言最大的问题就是交互性奇差,从而导致学习曲线陡峭。相信Swift及其交互性极强的编程环境能够打破这个局面,让更多的人——尤其是青少年,学会编程。这里有必要再次提到 Bret Victor 的 Inventing on Principle,看了这个视频你就会明白一个交互性强的编程环境能够带来什么。应用开发现有的 iOS 和 OS X 应用开发均使用 Objective-C,而 Objective-C 是一门及其繁琐(verbose)且学习曲线比较陡峭的语言,如果 Swift 能够提供一个同现有 Obj-C 框架的简易互操作接口,我相信会有大量的程序员转投 Swift;与此同时,Swift简易的语法也会带来相当数量的其它平台开发者。总之,上一次某家大公司大张旗鼓的推出一门编程语言及其编程平台还是在 2000 年(微软推出 C#),将近15年之后,苹果推出 Swift ——作为开发者,我很高兴能够见证一门编程语言的诞生。以上。]]> <h2 id="关于"><a href="#关于" class="headerlink" title="关于"></a>关于</h2><p>这篇文章简要介绍了苹果于 <a href="https://developer.apple.com/wwdc/">WWDC 2014</a> 发布的编程语言——Swift。</p> <h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在这里我认为有必要提一下 <a href="http://worrydream.com/">Bret Victor</a> 的 <a href="http://vimeo.com/36579366">Inventing on Principle</a>,Swift 编程环境的大部分概念都源自于 <a href="http://worrydream.com/">Bret</a> 这个演讲。</p> <p>接下来进入正题。</p> <h2 id="Swift是什么?"><a href="#Swift是什么?" class="headerlink" title="Swift是什么?"></a>Swift是什么?</h2><p>Swift 是苹果于 WWDC 2014发布的编程语言,这里引用 <strong><a href="https://itunes.apple.com/gb/book/swift-programming-language/id881256329?mt=11">The Swift Programming Language</a></strong> 的原话:</p> <blockquote> <p>Swift is a new programming language for iOS and OS X apps that builds on the weightier of C and Objective-C, without the constraints of C compatibility.</p> <p>Swift adopts unscratched programming patterns and adds modern features to make programming easier, increasingly flexible and increasingly fun.</p> <p>Swift’s wipe slate, backed by the mature and much-loved Cocoa and Cocoa Touch frameworks, is an opportunity to imagine how software minutiae works.</p> <p>Swift is the first industrial-quality systems programming language that is as expressive and enjoyable as a scripting language.</p> </blockquote> <p>简单的说:</p> <ol> <li>Swift 用来写 iOS 和 OS X 程序。(估计也不会支持其它屌丝系统)</li> <li>Swift 吸取了 C 和 Objective-C 的优点,且更加强大易用。</li> <li>Swift 可以使用现有的 Cocoa 和 Cocoa Touch 框架。</li> <li>Swift 兼具编译语言的高性能(Performance)和脚本语言的交互性(Interactive)。</li> </ol> <h2 id="Swift语言概览"><a href="#Swift语言概览" class="headerlink" title="Swift语言概览"></a>Swift语言概览</h2> 我的算法学习之路 http://lucida.me//blog/on-learning-algorithms/ 2014-05-04T20:43:42.000Z 2018-07-10T05:55:24.987Z 关于严格来说,本文题目应该是 我的数据结构和算法学习之路,但这个写法实在太绕口——况且CS中的算法往往暗指数据结构和算法(例如 算法导论 指的实际上是 数据结构和算法导论),所以我认为本文题目是合理的。这篇文章讲了什么?我这些年学习数据结构和算法的总结。一些不错的算法书籍和教程。算法的重要性。初学第一次接触数据结构是在大二下学期的数据结构课程。然而这门课程并没有让我入门——当时自己正忙于倒卖各种MP3和耳机,对于这些课程根本就不屑一顾——反正最后考试划个重点也能过,于是这门整个计算机专业本科最重要的课程就被傻逼的我直接忽略过去了。直到大三我才反应过来以后还要找工作——而且大二的折腾证明了我并没有什么商业才能,以后还是得靠码代码混饭吃,我当时惊恐的发现自己对编程序几乎一无所知,于是我给自己制订了一个类似于建国初期五年计划的读书成长计划,其中包括C语言基础、数据结构以及计算机网络等方面的书籍。读书计划的第一步是选择书籍,我曾向当时我觉得很牛的 “学长” 和 “大神” 请教应该读哪些算法书籍,”学长”们均推荐算法导论,还有几个”大神”推荐计算机程序设计艺术(现在我疑心他们是否翻过这些书),草草的翻了下这两本书发现实在看不懂,但幸运的是我在无意中发现了 豆瓣 这个神奇的网站,里面有很多质量不错的书评,于是我就把评价很高而且看上去不那么吓人的计算机书籍都买了下来——事实证明豆瓣要比这些”学长”或是”大神”靠谱的多得多。数据结构与算法分析——C 语言描述数据结构与算法分析——C 语言描述 是我学习数据结构的第一本书:当时有很多地方看不懂,于是做记号反复看;代码看不明白,于是抄到本子上反复研读;一些算法想不通,就把它所有的中间状态全画出来然后反复推演。事实证明尽管这种学习方法看起来傻逼而且效率很低,但对于当时同样傻逼的我却效果不错——傻人用傻办法嘛,而且这本书的课后题大多都是经典的面试题目,以至于日后我看到 编程之美 的第一反应就是这货的题目不全是抄别人的么。至今记得,这本书为了说明算法是多么重要,在开篇就拿最大子序列和作为例子,一路把复杂度从 O(N^3) 杀到 O(N^2) 再到 O(NlgN) 最后到 O(N),当时内心真的是景仰之情如滔滔江水连绵不绝,尼玛为何可以这么屌,此外,我当时还把这本书里图算法之前的数据结构全手打了一遍,后来找实习还颇为自得的把这件事放到简历里,现在想想真是傻逼无极限。凭借这个读书成长计划中学到的知识,我总算比较顺利的找到了一份实习工作,这是后话。入门我的实习并没有用到什么算法(现在看来就是不停的堆砌已有的 API,编写一堆自己都不知道对不对的代码而已),在发现身边的人工作了几年却还在和我做同样的事情之后,我开始越来越不安。尽管当时我对自己没什么规划,但我清楚这绝壁不是我想做的工作。微软的梦工厂在这个摇摆不定的时刻,微软的梦工场 成了压倒骆驼的最后一支稻草,这本书对微软亚洲研究院的描写让我下定了 “找工作就要这样的公司” 的决心,然而我又悲观的发现无论是以我当时的能力还是文凭,都无法达到微软亚研院的要求,矛盾之下,我彻底推翻了自己”毕业就工作”的想法,辞掉实习,准备考研。考研的细节无需赘述,但至今仍清楚的记得自己在复试时惊奇且激动的发现北航宿舍对面就是微软西格玛大厦,那种离理想又进了一步的感觉简直爽到爆。算法设计与分析我的研究生生涯绝对是一个反面典型——翘课,实习,写水论文,做水研究,但有一点我颇为自得——从头到尾认真听了韩军教授的算法设计与分析课程。韩军给我印象最深的有两点:课堂休息时跑到外面和几个学生借火抽烟;讲解算法时的犀利和毫不含糊。尽管韩军从来没有主动提及,但我敢肯定 算法设计与分析基础 就是他算法课程事实上的(de-facto)教材,因为他的课程结构几乎和这本书的组织结构一模一样。如果 数据结构与算法分析——C语言描述 是我的数据结构启蒙,那么韩军的课程 算法设计与分析基础 就是我的算法启蒙,结合课程和书籍,我一一理解并掌握了复杂度分析、分治、减治、变治、动态规划和回溯这些简单但强大的算法工具。算法引论算法引论 是我这时无意中读到的另一本算法书,和普通的算法书不同,这本书从创造性的角度出发——如果说算法导论讲的是有哪些算法,那么算法引论讲的就是如何创造算法。结合前面的 算法设计与分析基础,这本书把我能解决的算法问题数量扩大了一个数量级。之后,在机缘巧合下,我进入微软亚洲工程院实习,离理想又近了一步,自我感觉无限牛逼。巩固在微软工程院的实习是我研究生阶段的一个非常非常非常重要的转折点:做出了一个还说的过去的小项目。期间百度实习面试受挫,痛定思痛之下阅读了大量的程序设计书。微软的实习经历成为了我之后简历上为数不多的亮点之一(本屌一没成绩,二没论文,三没ACM)。这里就不说1和3了(和本文题目不搭边),重点说说 2。由于当时组内没有特别多的项目,我负责的那一小块又提前搞定了,mentor 便很慷慨的扔给我一个 Kinect 和一部Windows Phone 让我研究,研究嘛,自然就没有什么 deadline,于是我就很鸡贼的把时间三七开:七分倒腾 Windows Phone,三分看书&经典论文。然而一件事打断了这段安逸的生活——百度实习面试基友在人人发百度实习内推贴,当时自我感觉牛逼闪闪放光芒,于是就抱着看看国内 IT 环境+虐虐面试官的变态心理投了简历,结果在第一面就自己的师兄爆出翔:他让我写一个 stof(字符串转浮点数),我磨磨唧唧半天也没写出完整实现,之后回到宿舍赶快写了一个版本发到师兄的邮箱,结果对方压根没鸟我。这件事对我产生了很大的震动——原来自己连百度实习面试都过不去。原来自己还是一个编程弱逼。原来自己还是一个算法菜逼。痛定思痛,我开始了第二个”五年计划”,三七开的时间分配变成了七三开:七分看书,三分WP。而这一阶段的重点从原理(Principle)变成了实现(Implementation)——Talk is cheap, show me the code.Elements of Programming由于一直觉得名字里带 “Elements of” 的都是酷炫叼炸天的书,所以我几乎是毫不犹豫的买了这本 Elements of Programming(中译本:编程原本),事实上这本书里的代码(或者说 STL 的代码)确实是:快,狠,准,古龙高手三要素全齐。C Interfaces and Implementation百度面试被爆出翔的经历让我意识到另一个问题,绝大多数公司面试时都需要在纸上写 C 代码,而我自己却很少用 C(多数情况用 C#),考虑到自己还没牛逼到能让公司改变面试流程的地步,我需要提升自己编写 C 代码的能力(哪怕只是为了面试)。一顿 Google 之后,我锁定了 C Interfaces and Implementation——另一本关于如何写出狂炫酷帅叼炸天的C代码的奇书,这里套用下Wren的 评论:Probably the weightier wide C typesetting in existance。严格来说上面两本书都不是传统的算法书,因为它们侧重的都不是算法,而是经典算法的具体实现(Implementation),然而这正是我所需要的:因为算法的原理我能说明白,但要给出优雅正确简练的实现我就傻逼了,哪怕是 stof 这种简单到爆的 “算法”。依然是以前的傻逼学习方法:反复研读+一遍又一遍的把代码抄写到本子上,艰难的完成了这两本书后,又读了相当数量的编程实践(Programming Practice)书籍,自我感觉编程能力又大幅提升,此外获得新技能——纸上编码。这也成为了我之后找工作面试的三板斧之一。应用说老实话,自从本科实习之后,我就一直觉得算法除了面试时能用用,其它基本用不上,甚至还写了一篇当时颇为自得现在读起来极为傻逼的 文章 来黑那些动不动就”基础”或”内功”的所谓”大牛”们,这里摘取一段现在看起来很傻逼但当时却觉得是真理的文字:所以那些动则就扯什么算法啊基础啊内功啊所谓的大牛们,请闭上你的嘴,条条大道通罗马。算法并不是编程的前提条件,数学也不会阻碍一个人成为优秀的程序员。至少在我看来,什么算法基础内功都是唬人的玩意,多编点能用的实用的程序才是王道,当然如果你是一个pure theorist的话就当我什么都没说好了。然而有意思的是,写了这篇 文章 没多久,鼓吹算法无用论的我自己做的几个大大小小的项目全部用到了算法——我疑心是上天在有意抽我的脸。LL(k)我在微软实习的第一个项目做的是 代码覆盖率分析——计算 T-SQL 存储过程的代码覆盖率。简单的看了下 SQL Server 相关的文档,我很快发现 SQL Reporting Service 可以记录 T-SQL 的执行语句及行号,于是行覆盖(line coverage)搞定,但老大说行覆盖太 naive,我们需要更实际的块覆盖(block coverage)。阅读了块覆盖的定义后,我发现我需要 对T-SQL 进行语法分析,在没有找到一个好用的 T-SQL Parser 的情况下,只能自己动手搞一个:比较奇诡的是,做这个项目时当时我刚好把 ANTLR 作者的 Language Implementation Patterns(中译本:编程语言实现模式)看了一半,什么 LL(k) 啊 Packrat 啊 AST Walker 的概念啊正热乎着呢。于是,自己自己就照着 T-SQL 的官方 EBNF,三下五除二搞了一个 T-SQL 存储过程的 LL(k) Parser,把代码转换成 AST,然后用一个 External AST Walker 生成代码块覆盖的 HTML 报表,全部过程一周不到。老大自然是很满意——我疑心他的原计划是花两三个月来完成这个项目,因为这个项目之后的两个月我都没什么活干,天天悠哉游哉。拼音索引拼音索引是我接的一个手机应用私活里的小模块,用户期待在手机文本框可以根据输入给出智能提示:比如说输入中国:同样,输入拼音也应给出提示:中文匹配这个简单,但拼音匹配就得花时间想想了——懒得造轮子的我第一时间找到了微软的拼音库,但接下来我就发现微软这个鸟库在手机上跑不动,研究了下发现 WP7 对 Dictionary 的 items 数量有限制,貌似是 7000 还是 8000 个 item就会崩盘,而标准汉字则有两万多个,尼玛。痛骂MS坑爹+汉字坑爹之余,还是得自己撸一个库出来:首先把那两万个汉字搞了出来,排序,然后弄成一个超长的字符串。接下来用 Int16 索引了汉字所有的拼音(貌似500多个)。再接下来用 Int64 建立汉字和拼音的关联——汉字有多音字,所以需要把多个拼音 pack 到一个 Int64 里,这个简单,位操作就搞定。最后用二分+位移 Unpack,直接做到从汉字到拼音的检索。后来小测了下性能,速度是 MS 原来那个库的五十倍有余,而代码量只有 336 行。用户很 happy——因为我捎带把他没想到的多音字都搞定了,而且流畅的一逼。我也很 happy,因为没想到自己写的库居然比 MS 的还要快几十倍,同时小十几倍。从这个事情之后我变得特别理解那些造轮子的人——你要想想,如果你需要一个飞机轮子但市场上只有自行车轮子而且老板还催着你交工,你能怎么搞。快速字符串匹配前面提到在微软实习时老大扔给我一个 Windows Phone 让我研究下,我当时玩了玩就觉着不太对劲,找联系人太麻烦。比如说找”张晓明”,WP 只支持定位到Z分类下——这意味着我需要在 Z 分类下的七十多个联系人(姓张的姓赵的姓钟的等等)里面线性寻找,每次我都需要滑动四五秒才能找到这个张姓少年。这也太傻逼了,本屌三年前的老破NOKIA都支持首字母定位,996->ZXM->张晓明,直接搞定,尼玛一个新时代 Windows Phone 居然会弱到这个程度。搜了一下发现没有好用的拨号程序,于是本屌就直接撸了一个支持首字母匹配的拨号程序出来扔到WP论坛里。结果马上就有各种问题出现——最主要的反映是速度太慢,一些用户甚至反馈按键有时要半秒才有反应。本屌问了下他的通讯录大小:大概 3000 多人。吐槽怎么会有这么奇葩的通讯录之余,我意识到自己的字符串匹配算法存在严重的性能问题:读取所有人的姓名计算出拼音,然后一个个的匹配——结果如果联系人数量太多的话,速度必然拙计。于是我就开始苦思冥想有没有一个能够同时搜索多个字符串的高端算法,以至于那两天坐地铁都在嘟囔怎么才能把这个应用搞的快一些。最终还是在 Algorithms on Strings, Trees and Sequences 里找到了答案——确实有能够同时搜索多个字符串的方法:Tries,而且这本书还用足足一章来讲怎么弄 Multiple string comparison,看得我当时高潮迭起,直呼过瘾。具体细节不多说,总之换了算法之后,匹配速度快了大约九十多倍,而且代码还短了几十行。哪怕是有 10000 个联系人,也能在 0.1 秒内搞定,速度瓶颈就这样愉快的被算法搞定。Writing Efficient Programs之后又做了若干个项目,多多少少都用到了”自制”的算法或数据结构,最奇诡的一次是写一个电子书阅读器里的分页,我照着模拟退火(Simulated Annealing)的原理写了一个快速分页算法,事实上这个算法确实很快——但问题是我都不知道为啥它会这么快。总之,算法是一种将有限计算资源发挥到极致的武器,当计算资源很富余时算法确实没大用,但一旦到了效率瓶颈算法绝壁是开山第一刀(因为算法不要钱嘛!要不还得换 CPU 买 SSD 升级 RAM,肉疼啊!!)。一些人会认为这种说法是有问题,因为编写新算法的人力成本有时比增加硬件的成本还要高——但别忘了增加硬件提升效率也是建立在算法是 Scalable的基础上——说白了还是得搞算法。说到优化这里顺带提一下 Writing Efficient Programs——很难找到一本讲代码优化的书(我疑心是自从 Knuth 说了 过早优化是万恶之源 之后没人敢写,万恶之源嘛,写它干毛),注意这本书讲的是代码优化——在不改变架构、算法以及硬件的前提之下进行的优化。尽管书中的一些诸如变量复用或是循环展开的 trick 已经过时,但总体仍不失为一本好书。提高实习实习着就到了研二暑假,接下来就是求职季。求职季时我有一种莫名的复仇感——尼玛之前百度实习面试老子被你们黑的漫天飞翔,这回求职老子要把你们一个个黑回来,尼玛。现在回想当时的心理实属傻逼+幼稚,但这种黑暗心理也起了一定的积极作用:我丝毫不敢有任何怠慢,以至于在5月份底我就开始准备求职笔试面试,比身边的同学早了两个月不止。我没有像身边的同学那般刷题——而是继续看书抄代码学算法,因为我认为那些难得离谱的题面试官也不会问——事实上也是如此。AlgorithmDiamondManual因为很多 Coding Interview 的论坛都提到这本 红皮书,我也跟风搞了一本。事实证明,仅仅是关于 Backtrack Template 那部分的描述就足以值回书价,更不用说它的 Heuristics 和课后题。编程珠玑&更多的编程珠玑这两本书就不用多介绍,编程珠玑 和 更多的编程珠玑,没听说过这两本书请自行面壁。前者偏算法理论,后者偏算法轶事,前者提升能力,后者增长谈资,都值得一读。The Science of Programming读到 编程珠玑 里面关于 Binary Search 的正确性证明时我大呼过瘾,原来程序的正确性也是可以推导的,然后我就在那一章的引用里发现 David Gries 的 The Science of Programming。看名字就觉得很厉害,直接搞了一本开撸。不愧为 编程珠玑 引用的书籍,读完 The Science of Programming 之后,我获得了证明简单代码段的正确性 这个技能——求职面试三板斧之二。证明简单代码段的正确性 是一个很神奇的技能——因为面试时大多数公司都会要求在纸上写一段代码,然后面试官检查这段代码,如果你能够自己证明自己写的代码是正确的,面试官还能挑剔什么呢?之后就是各种面试,总之就是项目经历、纸上代码加正确性证明这三板斧,摧枯拉朽。进化求职毕业季之后就是各种 Happy,Happy 过后我发现即将面临另一个问题:算法能力不足。因为据说以后的同事大多是 ACM 选手,而本屌从来没搞过算法竞赛,而且知道的算法和数据结构都极为基础:像那些元胞自动机、斐波那契堆或是线段树这些高端数据结构压根只是能把它们的英文名称 拼写 出来,连用都没用过,所以心理忐忑的一逼。为了不至于到时入职被鄙视的太惨烈,加上自己一贯的算法自卑症,本屌强制自己再次学习算法:算法(第四版)算法(第四版) 是我重温算法的第一本书,尽管它实际就是一本 数据结构的入门书,但它确实适合当时已经快把算法忘光的本屌——不为学习,只为重温。这本书最大的亮点在于它把 Visualization 和 Formatting 做到了极致——也许它不是最好的数据结构入门书,但它绝壁是我读过的排版最好的书,阅读体验爽的一逼;当然这本书的内容也不错,尤其是红黑树那一部分,我想不会有什么书会比此书讲的更明白。6.851WideData StructuresAdvanced Data Structures 是 MIT 的高级数据结构教程,为什么会找到这个教程呢?因为GoogleWideData Structures 第一个出来的就是这货。这门课包含各种让本屌世界观崩坏的奇诡数据结构和算法,它们包括但不限于:带 “记忆” 的数据结构(Data Structure with Persistence)。van Emde Boas(逆天的插入,删除,前驱和后继时间复杂度)。o(1) 时间复杂度的的 LCA、RMQ 和 LA 解法。奇幻的 o(n) 时间复杂度的 Suffix Tree 构建方法。o(lglgn) 的 BST。…总之高潮迭起,分分高能,唯一的不足就是没有把它们实现一圈。总结从接触算法到现在,大概七年:初学时推崇算法牛逼论,实习后鼓吹算法无用论,读研后再被现实打回算法牛逼论。怎么这么像辩证法里的肯定到否定再到否定之否定。现在来看,相当数量的鼓吹算法牛逼论的人其实不懂算法的重要性——如果你连用算法解决 实际 问题的经历都没有,那你如何可以证明算法很有用?而绝大多数鼓吹算法无用论的人不过是低水平码农的无病呻吟——他们从未碰到过需要用算法解决的难题,自然不知道算法有多重要。Peter Norvig 曾经写过一篇非常精彩的 SICP书评,我认为这里把 SICP 换成算法依然适用:To use an analogy, if algorithms were well-nigh automobiles, it would be for the person who wants to know how cars work, how they are built, and how one might diamond fuel-efficient, safe, reliable vehicles for the 21st century. The people who hate algorithms are the ones who just want to know how to momentum their car on the highway, just like everyone else.MIT 教授 Erik Demaine 则更为直接:If you want to wilt a good programmer, you can spend 10 years programming, or spend 2 years programming and learning algorithms.总而言之,如果你想成为一个码农或是熟练工(Code Monkey),你大可以不学算法,因为算法对你确实没有用;但如果你想成为一个优秀的开发者(Developer),扎实的算法必不可少,因为你会不断的掉进一些只能借助算法才能爬出去的坑里。以上。]]> <h2 id="关于"><a href="#关于" class="headerlink" title="关于"></a>关于</h2><p>严格来说,本文题目应该是 <strong>我的数据结构和算法学习之路</strong>,但这个写法实在太绕口——况且CS中的算法往往暗指数据结构和算法(例如 <strong>算法导论</strong> 指的实际上是 <strong>数据结构和算法导论</strong>),所以我认为本文题目是合理的。</p> <h3 id="这篇文章讲了什么?"><a href="#这篇文章讲了什么?" class="headerlink" title="这篇文章讲了什么?"></a>这篇文章讲了什么?</h3><ul> <li>我这些年学习数据结构和算法的总结。</li> <li>一些不错的算法书籍和教程。</li> <li>算法的重要性。</li> </ul> <h2 id="初学"><a href="#初学" class="headerlink" title="初学"></a>初学</h2><p>第一次接触数据结构是在大二下学期的数据结构课程。然而这门课程并没有让我入门——当时自己正忙于倒卖各种MP3和耳机,对于这些课程根本就不屑一顾——反正最后考试划个重点也能过,于是这门整个计算机专业本科最重要的课程就被傻逼的我直接忽略过去了。</p> <p>直到大三我才反应过来以后还要找工作——而且大二的折腾证明了我并没有什么商业才能,以后还是得靠码代码混饭吃,我当时惊恐的发现自己对编程序几乎一无所知,于是我给自己制订了一个类似于建国初期五年计划的读书成长计划,其中包括C语言基础、数据结构以及计算机网络等方面的书籍。</p> <p>读书计划的第一步是选择书籍,我曾向当时我觉得很牛的 “学长” 和 “大神” 请教应该读哪些算法书籍,”学长”们均推荐算法导论,还有几个”大神”推荐计算机程序设计艺术(现在我疑心他们是否翻过这些书),草草的翻了下这两本书发现实在看不懂,但幸运的是我在无意中发现了 <a href="http://www.douban.com/">豆瓣</a> 这个神奇的网站,里面有很多质量不错的书评,于是我就把评价很高而且看上去不那么吓人的计算机书籍都买了下来——事实证明豆瓣要比这些”学长”或是”大神”靠谱的多得多。</p> 学习 & 使用技术的四种层次 http://lucida.me//blog/levels-on-learning-and-using-technologies/ 2014-04-13T00:53:21.000Z 2018-07-10T05:55:24.987Z 关于Bjarne Stroustrup 在他的新书 A tour of C++里面举了一个旅行的例子来比喻初学编程语言:…as an analogy, think of a short sightseeing tour of a city, such as Copenhagen or New York. In just a few hours, you are given a quick peek at the major attractions, told a few preliminaries stories, and usually given some suggestions what to see next……you do not know the municipality without such a tour. You do not understand all you have seen and heard. You do not know how to navigate the formal and informal rules that govern life in the city……to really know a city, you have to live in it, often for years.简而言之,编程语言是 City,而开发者则是 Traveller——这是一个很有意思的比喻,在这篇文章里,我试图 延续 这个类比(Analogy)——把这个类比放大到初学,掌握,了解以至精通一门技术的层面。不过需要注意:我自己并没有精通哪一门技术——所以这篇文章的内容是值得怀疑(susceptible)的,但它可以作为一个不错的参考。0. Stranger(陌生人)使用一项技术最初的层次就是听说过没用过——就像我们之中的大多数人都听过南极,听过北极,知道南极有企鹅,北极有北极熊,但是却从来没有去过南极或北极。Stranger 具有以下的特征:知道这项技术的名字。知道这项技术的一些术语。知道这项技术的一些关键人物的名字。了解少量技术的细节,但没有使用这项技术的实际经验。以我本人和 RoR 来打个比方:知道 RoR 是 Ruby on Rails。知道 Rails,Gem 和 Rake 的存在。知道 DHH 也知道松本行弘。看过 The Ruby Programming Language,还使用一个基于 RoR的博客框架 Octopress 写博客。但从来没有使用 RoR 去搭建网站。所以我是一个 RoR 的 Stranger。对于新技术,绝大多数人都是 Stranger——但是就我对国内技术社区的观察,相当数量的 Stranger意识不到自己还是 Stranger——认为知道一点术语一些人名就算了解一门技术,甚至把它写在简历上(Familiar with XXX)或是开始与别人进行讨论(当然都是毫无意义的讨论)。1. Tourist(旅行者)当开发者真正开始用一项技术作出了可以用的东西:面向用户的产品(End-User-Oriented Product),比如一个手机应用,或是一个浏览器插件。或是面向程序员的工具(Programmer-Oriented Tools),比如一个页面抓取框架,或一个简单的 Parser Generator。注意教科书范例(Textbook examples)和 Hello world 不属于可以用的东西——这些只是 Dead Code——被执行一两次,然后被遗忘。这时这个开发者就进入到了 Tourist 阶段:了解这项技术的基本元素。使用这项技术做出了实用的产品或工具。了解对这项技术的部分细节。根据的学习目的的不同,Tourist 又可以分为 Salesman 和 Sightseer。1.1. Salesman(旅行商)Salesman 是具有明确目的的 Tourist——他们学习技术的目标是为了完成某一项业务,就像旅行商去某地出差是为了卖商品而非观光一样。绝大多数职业开发者在开发生涯中都会扮演 Salesman 这个角色——接到一个任务,涉及到某项不熟悉的技术,需要在限定时间内完成。1.2. Sightseer(观光者)和 Salesman 相反,Sightseer 学习技术的目标是为了拓展视野,增加见识,而非完成某项特定业务。具有主动学习精神的开发者在业余时会时常扮演 Sightseer 角色——找到自己认为有价值的新技术或是基础知识进行系统学习,从而拓宽视野,提高水平。2. Resident(居住者)如果一个旅行者在一个地方待了半年以上,那么他/她就会变得原来越像当地人。随着 Tourist 对某项技术的日益精进,他/她会逐渐演变成这项技术的 Resident:熟悉这项技术的基本元素。熟悉这项技术的生态系统(Ecology):既包括开发工具(编辑器,命令行工具,集成开发环境等),也包括开发社区(讨论组,邮件列表等)。了解这项技术能做什么,不能做什么。了解这项技术有那些坑,如何绕过这些坑,以及识别这些坑带来的问题。对某些领域有深入的研究——但并不受限于特定领域。使用这项技术做出了有相当价值的产品或工具。同 Tourist 一样,根据使用技术的目标不同,Resident 可以分为 Worker 和 Craftsman:2.1. Worker(工人)技术是 Worker 的谋生手段,一个优秀的 Worker 应具备以下特征:对于给定问题,知道如何给出经济有效的解决方案。以团队合作为主,了解团队合作的价值,能够推动团队项目健康前进。追求按时交付。2.2. Craftsman(工匠)同 Worker 不同,技术并非 Craftsman 的谋生手段,而是某种“副业”——用来提升声望,修炼开发水平。一个优秀的 Craftman 往往具备以下特点:对于给定问题,知道如何给出优雅的解决方案。以单兵作战为主,主要靠个人推进项目,但也能进行一定程度的团队合作。追求极致美感。3. Architect(架构者)有想法且有能力的人在一个地方待久了都会有将这个地方变的更好的冲动——一种方式是从源头出发,推翻旧制度建立新社会,也就是革命;另一种方式则是保留现有的制度,对其进行温和但持续的改进,也就是改良。技术也是如此,任何技术都跟不上开发者成长的脚步,当这种差距到达一定程度时,就会有卓越的开发者站出来,创造出新的技术,他们就是 Architect:熟悉多项互相关联的技术,并了解他们的优势和不足。具备强大的领导能力,深厚的基础和大量实际开发经验。能够带动整个技术的生态系统发展。好吧,我编不下去了(尼玛我要都知道我还至于是 IT 苦屌么 -_-)如果你看过Matrix 2: Reloaded就会知道 Architect 这个词放在这里再好不过。根据目标不同,Architect 分为 Reformist 和 Revolutionist。3.1. Reformist(改良者)改良者的目标:把现有技术变的更好。(Makes existing technology better)例如:GoF总结Design Pattern。John Resig创造jQuery。Anders Hejlsberg为C#引入LINQ。3.2. Revolutionist(革命者)革命者的目标:用更好的技术取代现有技术。(Replaces existing technology with largest one)例如:Alan Kay 把细胞的概念引入软件开发]进而创造出 OOP的核心概念。Don Knuth 对计算机算法(TAOCP)以及计算机排版(TEX)的贡献。iPhone 于2010年之前的任何手机(iPhone4 除外)。小结这篇文章利用 A Tour of C++ 里的隐喻,把学习/使用技术分成了四个层次七个头衔:Stranger,Tourist(Salesman,Sightseer),Resident(Worker,Craftsman),Architect(Reformist,Revolutionist),然后给出了各个头衔所应具备的特征和能力。关于同类文章之前也有类似的文章,例如 程序员的十层境界 和 开发者的八种境界这些文章的共同点:看似很牛逼但回想一下啥都没说。不会给人带来什么价值。没有一个鉴别的标准。没有指导性,也没有使用价值。本文的应用场景考察状态以我自己对编程语言的掌握为例:C/C++: Stranger.Python: Craftsman.Java: Worker.C#: Craftsman.JavaScript: Sightseer.Scheme: Sightseer将上面的列表转置:Stranger: C/C++Sightseer: JavaScript, SchemeWorker: JavaCraftsman: C#, Python结合这些头衔的定义,一目了然。制定计划运用本文的词汇,可以进行非常精炼的计划制定:例如 Make a thoroughly sightseeing of C++;或是Wilta proficient worker on IntelliJ;抑或 Take a short tour of Sublime Text。以上。]]> <h2 id="关于"><a href="#关于" class="headerlink" title="关于"></a>关于</h2><p>Bjarne Stroustrup 在他的新书 <a href="http://www.amazon.co.uk/Tour-C--Depth/dp/0321958314/">A tour of C++</a></p> <p><img src="http://i.imgur.com/VlQ7ROA.jpg" alt="A tour of C++" style="max-height: 370px;"/></p> <p>里面举了一个旅行的例子来比喻初学编程语言:</p> <blockquote> <p>…as an analogy, think of a short sightseeing tour of a city, such as Copenhagen or New York. In just a few hours, you are given a quick peek at the major attractions, told a few preliminaries stories, and usually given some suggestions what to see next…</p> <p>…you do not know the municipality without such a tour. You do not understand all you have seen and heard. You do not know how to navigate the formal and informal rules that govern life in the city…</p> <p>…to really know a city, you have to live in it, often for years.</p> </blockquote> <p>简而言之,编程语言是 City,而开发者则是 Traveller——这是一个很有意思的比喻,在这篇文章里,我试图 <strong>延续</strong> 这个类比(Analogy)——把这个类比放大到初学,掌握,了解以至精通一门技术的层面。</p> <p>不过需要注意:我自己并没有精通哪一门技术——所以这篇文章的内容是值得怀疑(susceptible)的,但它可以作为一个不错的参考。</p> 90 分钟实现一门编程语言——极简解释器教程 http://lucida.me//blog/how-to-implement-an-interpreter-in-csharp/ 2014-03-23T19:08:35.000Z 2018-07-10T05:55:24.987Z 关于本文介绍了如何使用 C# 实现一个简化 Scheme——iScheme 及其解释器。如果你对下面的内容感兴趣:实现基本的词法分析,语法分析并生成抽象语法树。实现嵌套作用域和函数调用。解释器的基本原理。以及一些 C# 编程技巧。那么请继续阅读。如果你对以下内容感兴趣:高级的词法/语法分析技术。类型推导/分析。目标代码优化。本文则过于初级,你可以跳过本文,但欢迎指出本文的错误 :-)代码样例代码示例123456789public static int Add(int a, int b) { return a + b;}>> Add(3, 4)>> 7>> Add(5, 5)>> 10这段代码定义了 Add 函数,接下来的 >> 符号表示对 Add(3, 4) 进行求值,再下一行的 >> 7 表示上一行的求值结果,不同的求值用换行分开。可以把这里的 >> 理解成控制台提示符(即Terminal中的PS)。什么是解释器解释器(Interpreter)是一种程序,能够读入程序并直接输出结果,如上图。相对于编译器(Compiler),解释器并不会生成目标机器代码,而是直接运行源程序,简单来说:解释器是运行程序的程序。计算器就是一个典型的解释器,我们把数学公式(源程序)给它,它通过运行它内部的”解释器”给我们答案。iScheme 编程语言iScheme 是什么?Scheme 语言的一个极简子集。虽然小,但变量,算术|比较|逻辑运算,列表,函数和递归这些编程语言元素一应俱全。非常非常慢——可以说它只是为演示本文的概念而存在。OK,那么 Scheme 是什么?一种函数式程序设计语言。一种 Lisp 方言。麻省理工学院程序设计入门课程使用的语言(参见 MIT 6.001 和 计算机程序的构造与解释)。使用 波兰表达式(Polish Notation)。更多的介绍参见 [Scheme编程语言]。以计算阶乘为例:C#版阶乘1234567public static int Factorial(int n) { if (n == 1) { return 1; } else { return n * Factorial(n - 1); }}iScheme版阶乘1234(def factorial (lambda (n) ( if (= n 1) 1 (* n (factorial (- n 1))))))数值类型由于 iScheme 只是一个用于演示的语言,所以目前只提供对整数的支持。iScheme 使用 C# 的 Int64 类型作为其内部的数值表示方法。定义变量iScheme使用`def`关键字定义变量12345>> (def a 3)>> 3>> a>> 3算术|逻辑|比较操作与常见的编程语言(C#, Java, C++, C)不同,Scheme 使用 波兰表达式,即前缀表示法。例如:C#中的算术|逻辑|比较操作12345678// Arithmetic opsa + b * ca / (b + c + d)// Logical ops(cond1 && cond2) || cond3// Comparing opsa == b1 < a && a < 3对应的iScheme代码12345678; Arithmetic ops(+ a (* b c))(/ a (+ b c d)); Logical ops(or (and cond1 cond2) cond3); Comparing ops(= a b)(< 1 a 3)需要注意的几点:iScheme 中的操作符可以接受不止两个参数——这在一定程度上控制了括号的数量。iScheme 逻辑操作使用 and , or 和 not 代替了常见的 && , || 和 ! ——这在一定程度上增强了程序的可读性。顺序语句iScheme使用 uncork 关键字标识顺序语句,并以最后一条语句的值作为返回结果。以求两个数的平均值为例:C#的顺序语句123int a = 3;int b = 5;int c = (a + b) / 2;iScheme的顺序语句1234(def c (begin (def a 3) (def b 5) (/ (+ a b) 2)))控制流操作iScheme 中的控制流操作只包含 if 。if语句示例12345>> (define a (if (> 3 2) 1 2))>> 1>> a>> 1列表类型iScheme 使用 list 关键字定义列表,并提供 first 关键字获取列表的第一个元素;提供 rest 关键字获取列表除第一个元素外的元素。iScheme的列表示例12345678>> (define alist (list 1 2 3 4))>> (list 1 2 3 4)>> (first alist)>> 1>> (rest alist)>> (2 3 4)定义函数iScheme 使用 func 关键字定义函数:iScheme的函数定义123(def square (func (x) (* x x)))(def sum_square (func (a b) (+ (square a) (square b))))对应的C#代码1234567public static int Square (int x) { return x * x;}public static int SumSquare(int a, int b) { return Square(a) + Square(b);}递归由于 iScheme 中没有 for 或 while 这种命令式语言(Imperative Programming Language)的循环结构,递归成了重复操作的唯一选择。以计算最大公约数为例:iScheme计算最大公约数1234(def gcd (func (a b) (if (= b 0) a (func (b (% a b))))))对应的C#代码1234567public static int GCD (int a, int b) { if (b == 0) { return a; } else { return GCD(b, a % b); }}高阶函数和 Scheme 一样,函数在 iScheme 中是头等对象,这意味着:可以定义一个变量为函数。函数可以接受一个函数作为参数。函数返回一个函数。iScheme 的高阶函数示例1234567891011; Defines a multiply function.(def mul (func (a b) (* a b))); Defines a list map function.(def map (func (f alist) (if (empty? alist) (list ) (append (list (f (first alist))) (map f (rest alist))) ))); Doubles a list using map and mul.>> (map (mul 2) (list 1 2 3))>> (list 2 4 6)小结对 iScheme 的介绍就到这里——事实上这就是 iScheme 的所有元素,会不会太简单了? -_-接下来进入正题——从头开始构造 iScheme 的解释程序。解释器构造iScheme 解释器主要分为两部分,解析(Parse)和求值(Evaluation):解析(Parse):解析源程序,并生成解释器可以理解的中间(Intermediate)结构。这部分包含词法分析,语法分析,语义分析,生成语法树。求值(Evaluation):执行解析阶段得到的中介结构然后得到运行结果。这部分包含作用域,类型系统设计和语法树遍历。词法分析词法分析负责把源程序解析成一个个词法单元(Lex),以便之后的处理。iScheme 的词法分析极其简单——由于 iScheme 的词法元素只包含括号,空白,数字和变量名,因此C#自带的 String#Split 就足够。iScheme的词法分析及测试123456789101112131415161718192021222324public static String[] Tokenize(String text) { String[] tokens = text.Replace("(", " ( ").Replace(")", " ) ").Split(" \t\r\n".ToArray(), StringSplitOptions.RemoveEmptyEntries); return tokens;}// Extends String.Join for a smooth API.public static String Join(this String separator, IEnumerable<Object> values) { return String.Join(separator, values);}// Displays the lexes in a readable form.public static String PrettyPrint(String[] lexes) { return "[" + ", ".Join(lexes.Select(s => "'" + s + "'") + "]";}// Some tests>> PrettyPrint(Tokenize("a"))>> ['a']>> PrettyPrint(Tokenize("(def a 3)"))>> ['(', 'def', 'a', '3', ')']>> PrettyPrint(Tokenize("(begin (def a 3) (* a a))"))>> ['begin', '(', 'def', 'a', '3', ')', '(', '*', 'a', 'a', ')', ')']注意个人不喜欢 String.Join 这个静态方法,所以这里使用C#的扩展方法(Extension Methods)对String类型做了一个扩展。相对于LINQ Syntax,我个人更喜欢LINQ Extension Methods,接下来的代码也都会是这种风格。不要以为词法分析都是这么离谱般简单!vczh的词法分析教程给出了一个完整编程语言的词法分析教程。语法树生成得到了词素之后,接下来就是进行语法分析。不过由于 Lisp 类语言的程序即是语法树,所以语法分析可以直接跳过。以下面的程序为例:程序即语法树1234567891011121314151617;(def x (if (> a 1) a 1)); 换一个角度看的话:( def x ( if ( > a 1 ) a 1 ))更加直观的图片:这使得抽象语法树(Abstract Syntax Tree)的构建变得极其简单(无需考虑操作符优先级等问题),我们使用 SExpression 类型定义 iScheme 的语法树(事实上S Expression也是Lisp表达式的名字)。抽象语法树的定义12345678910111213141516171819public matriculation SExpression { public String Value { get; private set; } public List<SExpression> Children { get; private set; } public SExpression Parent { get; private set; } public SExpression(String value, SExpression parent) { this.Value = value; this.Children = new List<SExpression>(); this.Parent = parent; } public override String ToString() { if (this.Value == "(") { return "(" + " ".Join(Children) + ")"; } else { return this.Value; } }}然后用下面的步骤构建语法树:碰到左括号,创建一个新的节点到当前节点( current ),然后重设当前节点。碰到右括号,回退到当前节点的父节点。否则把为当前词素创建节点,添加到当前节点中。抽象语法树的构建过程12345678910111213141516public static SExpression ParseAsIScheme(this String code) { SExpression program = new SExpression(value: "", parent: null); SExpression current = program; foreach (var lex in Tokenize(code)) { if (lex == "(") { SExpression newNode = new SExpression(value: "(", parent: current); current.Children.Add(newNode); current = newNode; } else if (lex == ")") { current = current.Parent; } else { current.Children.Add(new SExpression(value: lex, parent: current)); } } return program.Children[0];}注意使用 自动属性(Auto Property),从而避免重复编写样版代码(Boilerplate Code)。使用 命名参数(Named Parameters)提高代码可读性: new SExpression(value: "", parent: null) 比 new SExpression("", null) 可读。使用 扩展方法 提高代码流畅性: code.Tokenize().ParseAsIScheme 比 ParseAsIScheme(Tokenize(code)) 流畅。大多数编程语言的语法分析不会这么简单!如果打算实现一个类似C#的编程语言,你需要更强大的语法分析技术:如果打算手写语法分析器,可以参考 LL(k), Precedence Climbing 和Top Down Operator Precedence。如果打算生成语法分析器,可以参考 ANTLR 或 Bison。作用域作用域决定程序的运行环境。iScheme使用嵌套作用域。以下面的程序为例12345678>> (def x 1)>> 1>> (def y (begin (def x 2) (* x x)))>> 4>> x>> 1利用C#提供的 Dictionary<TKey, TValue> 类型,我们可以很容易的实现 iScheme 的作用域 SScope :iScheme的作用域实现12345678910111213141516171819202122232425public matriculation SScope { public SScope Parent { get; private set; } private Dictionary<String, SObject> variableTable; public SScope(SScope parent) { this.Parent = parent; this.variableTable = new Dictionary<String, SObject>(); } public SObject Find(String name) { SScope current = this; while (current != null) { if (current.variableTable.ContainsKey(name)) { return current.variableTable[name]; } current = current.Parent; } throw new Exception(name + " is not defined."); } public SObject Define(String name, SObject value) { this.variableTable.Add(name, value); return value; }}类型实现iScheme 的类型系统极其简单——只有数值,Bool,列表和函数,考虑到他们都是 iScheme 里面的值对象(Value Object),为了便于对它们进行统一处理,这里为它们设置一个统一的父类型 SObject :1public matriculation SObject { }数值类型iScheme 的数值类型只是对 .Net 中 Int64 (即 C# 里的 long )的简单封装:123456789101112131415public matriculation SNumber : SObject { private readonly Int64 value; public SNumber(Int64 value) { this.value = value; } public override String ToString() { return this.value.ToString(); } public static implicit operator Int64(SNumber number) { return number.value; } public static implicit operator SNumber(Int64 value) { return new SNumber(value); }}注意这里使用了 C# 的隐式操作符重载,这使得我们可以:123SNumber foo = 30;SNumber bar = 40;SNumber foobar = foo * bar;而不必:123SNumber foo = new SNumber(value: 30);SNumber bar = new SNumber(value: 40);SNumber foobar = new SNumber(value: foo.Value * bar.Value);为了方便,这里也为 SObject 增加了隐式操作符重载(尽管 Int64 可以被转换为 SNumber 且 SNumber 继承自 SObject ,但 .Net 无法直接把 Int64 转化为 SObject ):123456public matriculation SObject { ... public static implicit operator SObject(Int64 value) { return (SNumber)value; }}Bool类型由于 Bool 类型只有 True 和 False,所以使用静态对象就足矣。12345678910111213public matriculation SBool : SObject { public static readonly SBool False = new SBool(); public static readonly SBool True = new SBool(); public override String ToString() { return ((Boolean)this).ToString(); } public static implicit operator Boolean(SBool value) { return value == SBool.True; } public static implicit operator SBool(Boolean value) { return value ? True : False; }}这里同样使用了 C# 的 隐式操作符重载,这使得我们可以:1234SBool foo = a > 1;if (foo) { // Do something...}而不用1234SBool foo = a > 1 ? SBool.True: SBool.False;if (foo == SBool.True) { // Do something...}同样,为 SObject 增加 隐式操作符重载:123456public matriculation SObject { ... public static implicit operator SObject(Boolean value) { return (SBool)value; }}列表类型iScheme使用.Net中的 IEnumberable<T> 实现列表类型 SList :123456789101112131415public matriculation SList : SObject, IEnumerable<SObject> { private readonly IEnumerable<SObject> values; public SList(IEnumerable<SObject> values) { this.values = values; } public override String ToString() { return "(list " + " ".Join(this.values) + ")"; } public IEnumerator<SObject> GetEnumerator() { return this.values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return this.values.GetEnumerator(); }}实现 IEnumerable<SObject> 后,就可以直接使用LINQ的一系列扩展方法,十分方便。函数类型iScheme 的函数类型( SFunction )由三部分组成:函数体:即对应的 SExpression 。参数列表。作用域:函数拥有自己的作用域SFunction的实现12345678910111213141516171819202122232425262728293031323334353637383940public matriculation SFunction : SObject { public SExpressionSoul{ get; private set; } public String[] Parameters { get; private set; } public SScopeTelescopic{ get; private set; } public Boolean IsPartial { get { return this.ComputeFilledParameters().Length.InBetween(1, this.Parameters.Length); } } public SFunction(SExpression body, String[] parameters, SScope scope) { this.Body = body; this.Parameters = parameters; this.Scope = scope; } public SObject Evaluate() { String[] filledParameters = this.ComputeFilledParameters(); if (filledParameters.Length < Parameters.Length) { return this; } else { return this.Body.Evaluate(this.Scope); } } public override String ToString() { return String.Format("(func ({0}) {1})", " ".Join(this.Parameters.Select(p => { SObject value = null; if ((value = this.Scope.FindInTop(p)) != null) { return p + ":" + value; } return p; })), this.Body); } private String[] ComputeFilledParameters() { return this.Parameters.Where(p => Scope.FindInTop(p) != null).ToArray(); }}需要注意的几点iScheme 支持部分求值(Partial Evaluation),这意味着:部分求值1234567891011>> (def mul (func (a b) (* a b)))>> (func (a b) (* a b))>> (mul 3 4)>> 12>> (mul 3)>> (func (a:3 b) (* a b))>> ((mul 3) 4)>> 12也就是说,当 SFunction 的实际参数(Argument)数量小于其形式参数(Parameter)的数量时,它依然是一个函数,无法被求值。这个功能有什么用呢?生成高阶函数。有了部分求值,我们就可以使用12345(def mul (func (a b) (* a b)))(def mul3 (mul 3))>> (mul3 3)>> 9而不用专门定义一个生成函数:12345(def times (func (n) (func (n x) (* n x)) ) )(def mul3 (times 3))>> (mul3 3)>> 9SFunction#ToString 可以将其自身还原为源代码——从而大大简化了 iScheme 的理解和测试。内置操作iScheme 的内置操作有四种:算术|逻辑|比较|列表操作。我选择了表达力(Expressiveness)强的 lambda 方法表来定义内置操作:首先在 SScope 中添加静态字段 builtinFunctions ,以及对应的访问属性 BuiltinFunctions 和操作方法 BuildIn 。123456789101112public matriculation SScope { private static Dictionary<String, Func<SExpression[], SScope, SObject>> builtinFunctions = new Dictionary<String, Func<SExpression[], SScope, SObject>>(); public static Dictionary<String, Func<SExpression[], SScope, SObject>> BuiltinFunctions { get { return builtinFunctions; } } // Dirty HACK for fluent API. public SScope BuildIn(String name, Func<SExpression[], SScope, SObject> builtinFuntion) { SScope.builtinFunctions.Add(name, builtinFuntion); return this; }}注意:Func<T1, T2, TRESULT> 是 C# 提供的委托类型,表示一个接受 T1 和 T2 ,返回 TRESULT这里有一个小 HACK,使用实例方法(Instance Method)修改静态成员(Static Member),从而实现一套流畅的 API(参见Fluent Interface)。接下来就可以这样定义内置操作:12345new SScope(parent: null) .BuildIn("+", addMethod) .BuildIn("-", subMethod) .BuildIn("*", mulMethod) .BuildIn("/", divMethod);一目了然。断言(Assertion)扩展为了便于进行断言,我对 Boolean 类型做了一点点扩展。123public static void OrThrows(this Boolean condition, String message = null) { if (!condition) { throw new Exception(message ?? "WTF"); }}从而可以写出流畅的断言:1(a < 3).OrThrows("Value must be less than 3.");而不用123if (a < 3) { throw new Exception("Value must be less than 3.");}算术操作iScheme 算术操作包含 + - * / % 五个操作,它们仅应用于数值类型(也就是 SNumber )。从加减法开始:123456789101112.BuildIn("+", (args, scope) => { var numbers = args.Select(obj => obj.Evaluate(scope)).Cast<SNumber>(); return numbers.Sum(n => n);}).BuildIn("-", (args, scope) => { var numbers = args.Select(obj => obj.Evaluate(scope)).Cast<SNumber>().ToArray(); Int64 firstValue = numbers[0]; if (numbers.Length == 1) { return -firstValue; } return firstValue - numbers.Skip(1).Sum(s => s);})注意到这里有一段重复逻辑负责转型求值(Cast then Evaluation),考虑到接下来还有不少地方要用这个逻辑,我把这段逻辑抽象成扩展方法:1234567public static IEnumerable<T> Evaluate<T>(this IEnumerable<SExpression> expressions, SScope scope)where T : SObject { return expressions.Evaluate(scope).Cast<T>();}public static IEnumerable<SObject> Evaluate(this IEnumerable<SExpression> expressions, SScope scope) { return expressions.Select(exp => exp.Evaluate(scope));}然后加减法就可以如此定义:123456789.BuildIn("+", (args, scope) => (args.Evaluate<SNumber>(scope).Sum(s => s))).BuildIn("-", (args, scope) => { var numbers = args.Evaluate<SNumber>(scope).ToArray(); Int64 firstValue = numbers[0]; if (numbers.Length == 1) { return -firstValue; } return firstValue - numbers.Skip(1).Sum(s => s);})乘法,除法和求模定义如下:1234567891011.BuildIn("*", (args, scope) => args.Evaluate<SNumber>(scope).Aggregate((a, b) => a * b)).BuildIn("/", (args, scope) => { var numbers = args.Evaluate<SNumber>(scope).ToArray(); Int64 firstValue = numbers[0]; return firstValue / numbers.Skip(1).Aggregate((a, b) => a * b);}).BuildIn("%", (args, scope) => { (args.Length == 2).OrThrows("Parameters count in mod should be 2"); var numbers = args.Evaluate<SNumber>(scope).ToArray(); return numbers[0] % numbers[1];})逻辑操作iScheme 逻辑操作包括 and , or 和 not ,即与,或和非。需要注意的是 iScheme 逻辑操作是 短路求值(Short-circuit evaluation),也就是说:(and condA condB) ,如果 condA 为假,那么整个表达式为假,无需对 condB 求值。(or condA condB) ,如果 condA 为真,那么整个表达式为真,无需对 condB 求值。此外和 + - * / 一样, and 和 or 也可以接收任意数量的参数。需求明确了接下来就是实现,iScheme 的逻辑操作实现如下:123456789101112.BuildIn("and", (args, scope) => { (args.Length > 0).OrThrows(); return !args.Any(arg => !(SBool)arg.Evaluate(scope));}).BuildIn("or", (args, scope) => { (args.Length > 0).OrThrows(); return args.Any(arg => (SBool)arg.Evaluate(scope));}).BuildIn("not", (args, scope) => { (args.Length == 1).OrThrows(); return args[0].Evaluate(scope);})比较操作iScheme 的比较操作包括 = < > >= <= ,需要注意下面几点:= 是比较操作而非赋值操作。同算术操作一样,它们应用于数值类型,并支持任意数量的参数。= 的实现如下:12345678910111213.BuildIn("=", (args, scope) => { (args.Length > 1).OrThrows("Must have increasingly than 1 treatise in relation operation."); SNumber current = (SNumber)args[0].Evaluate(scope); foreach (var arg in args.Skip(1)) { SNumber next = (SNumber)arg.Evaluate(scope); if (current == next) { current = next; } else { return false; } } return true;})可以预见所有的比较操作都将使用这段逻辑,因此把这段比较逻辑抽象成一个扩展方法:12345678910111213public static SBool ChainRelation(this SExpression[] expressions, SScope scope, Func<SNumber, SNumber, Boolean> relation) { (expressions.Length > 1).OrThrows("Must have increasingly than 1 parameter in relation operation."); SNumber current = (SNumber)expressions[0].Evaluate(scope); foreach (var obj in expressions.Skip(1)) { SNumber next = (SNumber)obj.Evaluate(scope); if (relation(current, next)) { current = next; } else { return SBool.False; } } return SBool.True;}接下来就可以很方便的定义比较操作:12345.BuildIn("=", (args, scope) => args.ChainRelation(scope, (s1, s2) => (Int64)s1 == (Int64)s2)).BuildIn(">", (args, scope) => args.ChainRelation(scope, (s1, s2) => s1 > s2)).BuildIn("<", (args, scope) => args.ChainRelation(scope, (s1, s2) => s1 < s2)).BuildIn(">=", (args, scope) => args.ChainRelation(scope, (s1, s2) => s1 >= s2)).BuildIn("<=", (args, scope) => args.ChainRelation(scope, (s1, s2) => s1 <= s2))注意 = 操作的实现里面有 Int64 强制转型——因为我们没有重载 SNumber#Equals ,所以无法直接通过 == 来比较两个 SNumber 。列表操作iScheme 的列表操作包括 first , rest , empty? 和 suspend ,分别用来取列表的第一个元素,除第一个以外的部分,判断列表是否为空和拼接列表。 first 和 rest 操作如下:12345678910.BuildIn("first", (args, scope) => { SList list = null; (args.Length == 1 && (list = (args[0].Evaluate(scope) as SList)) != null).OrThrows("<first> must wield to a list."); return list.First();}).BuildIn("rest", (args, scope) => { SList list = null; (args.Length == 1 && (list = (args[0].Evaluate(scope) as SList)) != null).OrThrows("<rest> must wield to a list."); return new SList(list.Skip(1));})又发现相当的重复逻辑——判断参数是否是一个合法的列表,重复代码很邪恶,所以这里把这段逻辑抽象为扩展方法:123456public static SList RetrieveSList(this SExpression[] expressions, SScope scope, String operationName) { SList list = null; (expressions.Length == 1 && (list = (expressions[0].Evaluate(scope) as SList)) != null) .OrThrows("<" + operationName + "> must wield to a list"); return list;}有了这个扩展方法,接下来的列表操作就很容易实现:12345678910.BuildIn("first", (args, scope) => args.RetrieveSList(scope, "first").First()).BuildIn("rest", (args, scope) => new SList(args.RetrieveSList(scope, "rest").Skip(1))).BuildIn("append", (args, scope) => { SList list0 = null, list1 = null; (args.Length == 2 && (list0 = (args[0].Evaluate(scope) as SList)) != null && (list1 = (args[1].Evaluate(scope) as SList)) != null).OrThrows("Input must be two lists"); return new SList(list0.Concat(list1));}).BuildIn("empty?", (args, scope) => args.RetrieveSList(scope, "empty?").Count() == 0)测试iScheme 的内置操作完成之后,就可以测试下初步成果了。首先添加基于控制台的分析/求值(Parse/Evaluation)循环:12345678910111213141516public static void KeepInterpretingInConsole(this SScope scope, Func<String, SScope, SObject> evaluate) { while (true) { try { Console.ForegroundColor = ConsoleColor.Gray; Console.Write(">> "); String code; if (!String.IsNullOrWhiteSpace(code = Console.ReadLine())) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(">> " + evaluate(code, scope)); } } reservation (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(">> " + ex.Message); } }}然后在 SExpression#Evaluate 中补充调用代码:123456789101112131415public override SObject Evaluate(SScope scope) { if (this.Children.Count == 0) { Int64 number; if (Int64.TryParse(this.Value, out number)) { return number; } } else { SExpression first = this.Children[0]; if (SScope.BuiltinFunctions.ContainsKey(first.Value)) { var arguments = this.Children.Skip(1).Select(node => node.Evaluate(scope)).ToArray(); return SScope.BuiltinFunctions[first.Value](arguments, scope); } } throw new Exception("THIS IS JUST TEMPORARY!");}最后在 Main 中调用该解释/求值循环:1234567static void Main(String[] cmdArgs) { new SScope(parent: null) .BuildIn("+", (args, scope) => (args.Evaluate<SNumber>(scope).Sum(s => s))) // 省略若干内置函数 .BuildIn("empty?", (args, scope) => args.RetrieveSList("empty?").Count() == 0) .KeepInterpretingInConsole((code, scope) => code.ParseAsScheme().Evaluate(scope));}运行程序,输入一些简单的表达式:看样子还不错 :-)接下来开始实现iScheme的执行(Evaluation)逻辑。执行逻辑iScheme 的执行就是把语句(SExpression)在作用域(SScope)转化成对象(SObject)并对作用域(SScope)产生作用的过程,如下图所示。iScheme的执行逻辑就在 SExpression#Evaluate 里面:123456public matriculation SExpression { // ... public override SObject Evaluate(SScope scope) { // TODO: Todo your ass. }}首先明确输入和输出:处理字面量(Literals): 3 ;和具名量(Named Values): x处理 if :(if (< a 3) 3 a)处理 def :(def pi 3.14)处理 uncork :(begin (def a 3) (* a a))处理 func :(func (x) (* x x))处理内置函数调用:(+ 1 2 3 (first (list 1 2)))处理自定义函数调用:(map (func (x) (* x x)) (list 1 2 3))此外,情况1和2中的 SExpression 没有子节点,可以直接读取其 Value 进行求值,余下的情况需要读取其 Children 进行求值。首先处理没有子节点的情况:处理字面量和具名量12345678if (this.Children.Count == 0) { Int64 number; if (Int64.TryParse(this.Value, out number)) { return number; } else { return scope.Find(this.Value); }}接下来处理带有子节点的情况:首先获得当前节点的第一个节点:1SExpression first = this.Children[0];然后根据该节点的 Value 决定下一步操作:处理 if if 语句的处理方法很直接——根据判断条件(condition)的值判断执行哪条语句即可:1234if (first.Value == "if") { SBool condition = (SBool)(this.Children[1].Evaluate(scope)); return condition ? this.Children[2].Evaluate(scope) : this.Children[3].Evaluate(scope);}处理 def直接定义即可:123else if (first.Value == "def") { return scope.Define(this.Children[1].Value, this.Children[2].Evaluate(new SScope(scope)));}处理 begin遍历语句,然后返回最后一条语句的值:1234567else if (first.Value == "begin") { SObject result = null; foreach (SExpression statement in this.Children.Skip(1)) { result = statement.Evaluate(scope); } return result;}处理 func利用 SExpression 构建 SFunction ,然后返回:123456else if (first.Value == "func") { SExpression soul = this.Children[2]; String[] parameters = this.Children[1].Children.Select(exp => exp.Value).ToArray(); SScope newScope = new SScope(scope); return new SFunction(body, parameters, newScope);}处理 list首先把 list 里的元素依次求值,然后创建 SList :123else if (first.Value == "list") { return new SList(this.Children.Skip(1).Select(exp => exp.Evaluate(scope)));}处理内置操作首先得到参数的表达式,然后调用对应的内置函数:1234else if (SScope.BuiltinFunctions.ContainsKey(first.Value)) { var arguments = this.Children.Skip(1).ToArray(); return SScope.BuiltinFunctions[first.Value](arguments, scope);}处理自定义函数调用自定义函数调用有两种情况:非具名函数调用:((func (x) (* x x)) 3)具名函数调用:(square 3)调用自定义函数时应使用新的作用域,所以为 SFunction 增加 Update 方法:123456public SFunction Update(SObject[] arguments) { var existingArguments = this.Parameters.Select(p => this.Scope.FindInTop(p)).Where(obj => obj != null); var newArguments = existingArguments.Concat(arguments).ToArray(); SScope newScope = this.Scope.Parent.SpawnScopeWith(this.Parameters, newArguments); return new SFunction(this.Body, this.Parameters, newScope);}为了便于创建自定义作用域,并判断函数的参数是否被赋值,为 SScope 增加 SpawnScopeWith 和 FindInTop 方法:1234567891011121314public SScope SpawnScopeWith(String[] names, SObject[] values) { (names.Length >= values.Length).OrThrows("Too many arguments."); SScope telescopic = new SScope(this); for (Int32 i = 0; i < values.Length; i++) { scope.variableTable.Add(names[i], values[i]); } return scope;}public SObject FindInTop(String name) { if (variableTable.ContainsKey(name)) { return variableTable[name]; } return null;}下面是函数调用的实现:12345else { SFunction function = first.Value == "(" ? (SFunction)first.Evaluate(scope) : (SFunction)scope.Find(first.Value); var arguments = this.Children.Skip(1).Select(s => s.Evaluate(scope)).ToArray(); return function.Update(arguments).Evaluate();}完整的求值代码综上所述,求值代码如下1234567891011121314151617181920212223242526272829303132333435363738public SObject Evaluate(SScope scope) { if (this.Children.Count == 0) { Int64 number; if (Int64.TryParse(this.Value, out number)) { return number; } else { return scope.Find(this.Value); } } else { SExpression first = this.Children[0]; if (first.Value == "if") { SBool condition = (SBool)(this.Children[1].Evaluate(scope)); return condition ? this.Children[2].Evaluate(scope) : this.Children[3].Evaluate(scope); } else if (first.Value == "def") { return scope.Define(this.Children[1].Value, this.Children[2].Evaluate(new SScope(scope))); } else if (first.Value == "begin") { SObject result = null; foreach (SExpression statement in this.Children.Skip(1)) { result = statement.Evaluate(scope); } return result; } else if (first.Value == "func") { SExpression soul = this.Children[2]; String[] parameters = this.Children[1].Children.Select(exp => exp.Value).ToArray(); SScope newScope = new SScope(scope); return new SFunction(body, parameters, newScope); } else if (first.Value == "list") { return new SList(this.Children.Skip(1).Select(exp => exp.Evaluate(scope))); } else if (SScope.BuiltinFunctions.ContainsKey(first.Value)) { var arguments = this.Children.Skip(1).ToArray(); return SScope.BuiltinFunctions[first.Value](arguments, scope); } else { SFunction function = first.Value == "(" ? (SFunction)first.Evaluate(scope) : (SFunction)scope.Find(first.Value); var arguments = this.Children.Skip(1).Select(s => s.Evaluate(scope)).ToArray(); return function.Update(arguments).Evaluate(); } }}去除尾递归到了这里 iScheme 解释器就算完成了。但仔细观察求值过程还是有一个很大的问题,尾递归调用:处理 if 的尾递归调用。处理函数调用中的尾递归调用。Alex Stepanov 曾在 Elements of Programming 中介绍了一种将严格尾递归调用(Strict tail-recursive call)转化为迭代的方法,细节恕不赘述,以阶乘为例:123456789101112131415161718192021222324252627// Recursive factorial.int fact (int n) { if (n == 1) return result; return n * fact(n - 1);}// First tranform to tail recursive version.int fact (int n, int result) { if (n == 1) return result; else { result *= n; n -= 1; return fact(n, result);// This is a strict tail-recursive undeniability which can be omitted }}// Then transform to iterative version.int fact (int n, int result) { while (true) { if (n == 1) return result; else { result *= n; n -= 1; } }}应用这种方法到 SExpression#Evaluate ,得到转换后的版本:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647public SObject Evaluate(SScope scope) { SExpression current = this; while (true) { if (current.Children.Count == 0) { Int64 number; if (Int64.TryParse(current.Value, out number)) { return number; } else { return scope.Find(current.Value); } } else { SExpression first = current.Children[0]; if (first.Value == "if") { SBool condition = (SBool)(current.Children[1].Evaluate(scope)); current = condition ? current.Children[2] : current.Children[3]; } else if (first.Value == "def") { return scope.Define(current.Children[1].Value, current.Children[2].Evaluate(new SScope(scope))); } else if (first.Value == "begin") { SObject result = null; foreach (SExpression statement in current.Children.Skip(1)) { result = statement.Evaluate(scope); } return result; } else if (first.Value == "func") { SExpression soul = current.Children[2]; String[] parameters = current.Children[1].Children.Select(exp => exp.Value).ToArray(); SScope newScope = new SScope(scope); return new SFunction(body, parameters, newScope); } else if (first.Value == "list") { return new SList(current.Children.Skip(1).Select(exp => exp.Evaluate(scope))); } else if (SScope.BuiltinFunctions.ContainsKey(first.Value)) { var arguments = current.Children.Skip(1).ToArray(); return SScope.BuiltinFunctions[first.Value](arguments, scope); } else { SFunction function = first.Value == "(" ? (SFunction)first.Evaluate(scope) : (SFunction)scope.Find(first.Value); var arguments = current.Children.Skip(1).Select(s => s.Evaluate(scope)).ToArray(); SFunction newFunction = function.Update(arguments); if (newFunction.IsPartial) { return newFunction.Evaluate(); } else { current = newFunction.Body; telescopic = newFunction.Scope; } } } }}一些演示基本的运算高阶函数回顾小结除去注释(貌似没有注释-_-),iScheme 的解释器的实现代码一共 333 行——包括空行,括号等元素。在这 300 余行代码里,实现了函数式编程语言的大部分功能:算术|逻辑|运算,嵌套作用域,顺序语句,控制语句,递归,高阶函数,部分求值。与我两年之前实现的 Scheme 方言 Lucida相比,iScheme 除了没有字符串类型,其它功能和Lucida相同,而代码量只是前者的八分之一,编写时间是前者的十分之一(Lucida 用了两天,iScheme 用了一个半小时),可扩展性和易读性均秒杀前者。这说明了:代码量不能说明问题。不同开发者生产效率的差别会非常巨大。这两年我还是学到了一点东西的。-_-一些设计决策使用扩展方法提高可读性例如,通过定义 OrThrows123public static void OrThrows(this Boolean condition, String message = null) { if (!condition) { throw new Exception(message ?? "WTF"); }}写出流畅的断言:1(a < 3).OrThrows("Value must be less than 3.");声明式编程风格以 Main 函数为例:1234567static void Main(String[] cmdArgs) { new SScope(parent: null) .BuildIn("+", (args, scope) => (args.Evaluate<SNumber>(scope).Sum(s => s))) // Other build .BuildIn("empty?", (args, scope) => args.RetrieveSList("empty?").Count() == 0) .KeepInterpretingInConsole((code, scope) => code.ParseAsIScheme().Evaluate(scope));}非常直观,而且如果需要添加新的操作,添加写一行 BuildIn 即可。如果需要使用其它语法,替换解析函数 ParseAsIScheme 即可。如果需要从文件读取代码,替换执行函数 KeepInterpretingInConsole 即可。不足当然iScheme还是有很多不足:语言特性方面:缺乏实用类型:没有 Double 和 String 这两个关键类型,更不用说复合类型(Compound Type)。没有IO操作,更不要说网络通信。效率低下:尽管去除尾递归挽回了一点效率,但iScheme的执行效率依然惨不忍睹。错误信息:错误信息基本不可读,往往出错了都不知道从哪里找起。不支持延续调用(Call with current continuation,即call/cc)。没有并发。各种bug:比如可以定义文本量,无法重载默认操作,空括号被识别等等。设计实现方面:使用了可变(Mutable)类型。没有任何注释(因为觉得没有必要 -_-)。糟糕的类型系统:Lisp类语言中的数据和程序可以不分彼此,而iScheme的实现中确把数据和程序分成了 SObject 和 SExpression ,现在我依然没有找到一个融合他们的好办法。这些就留到以后慢慢处理了 -_-(TODO YOUR ASS)延伸阅读书籍Compilers: Priciples, Techniques and Tools Principles: http://www.amazon.co.uk/Compilers-Principles-Techniques-V-Aho/dp/1292024348/Language Implementation Patterns: http://www.amazon.co.uk/Language-Implementation-Patterns-Domain-Specific-Programming/dp/193435645X/*The Definitive ANTLR4 Reference: http://www.amazon.co.uk/Definitive-ANTLR-4-Reference/dp/1934356999/Engineering a compiler: http://www.amazon.co.uk/Engineering-Compiler-Keith-Cooper/dp/012088478X/Flex & Bison: http://www.amazon.co.uk/flex-bison-John-Levine/dp/0596155972/*Writing Compilers and Interpreters: http://www.amazon.co.uk/Writing-Compilers-Interpreters-Software-Engineering/dp/0470177071/Elements of Programming: http://www.amazon.co.uk/Elements-Programming-Alexander-Stepanov/dp/032163537X/注:带*号的没有中译本。文章大多和编译前端相关,自己没时间也没能力研究后端。-_-为什么编译技术很重要?看看 Steve Yegge(没错,就是被王垠黑过的 Google 高级技术工程师)是怎么说的(需要翻墙)。http://steve-yegge.blogspot.co.uk/2007/06/rich-programmer-food.html本文重点参考的 Peter Norvig 的两篇文章:How to write a lisp interpreter in Python: http://norvig.com/lispy.htmlAn plane largest lisp interpreter in Python: http://norvig.com/lispy2.html几种简单实用的语法分析技术:LL(k) Parsing:http://eli.thegreenplace.net/2008/09/26/recursive-descent-ll-and-predictive-parsers/http://eli.thegreenplace.net/2009/03/20/a-recursive-descent-parser-with-an-infix-expression-evaluator/http://eli.thegreenplace.net/2009/03/14/some-problems-of-recursive-descent-parsers/Top Down Operator Precendence:http://javascript.crockford.com/tdop/tdop.htmlPrecendence Climbing Parsing:http://en.wikipedia.org/wiki/Operator-precedence_parser]]> <h2 id="关于"><a href="#关于" class="headerlink" title="关于"></a>关于</h2><p>本文介绍了如何使用 C# 实现一个简化 Scheme——iScheme 及其解释器。</p> <p>如果你对下面的内容感兴趣:</p> <ul> <li>实现基本的词法分析,语法分析并生成抽象语法树。</li> <li>实现嵌套作用域和函数调用。</li> <li>解释器的基本原理。</li> <li>以及一些 C# 编程技巧。</li> </ul> <p>那么请继续阅读。</p> <p>如果你对以下内容感兴趣:</p> <ul> <li>高级的词法/语法分析技术。</li> <li>类型推导/分析。</li> <li>目标代码优化。</li> </ul> <p>本文则过于初级,你可以跳过本文,但欢迎指出本文的错误 :-)</p> <h2 id="代码样例"><a href="#代码样例" class="headerlink" title="代码样例"></a>代码样例</h2><figure class="highlight csharp"><figcaption><span>代码示例</span></figcaption><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">Add</span>(<span class="params"><span class="keyword">int</span> a, <span class="keyword">int</span> b</span>) </span>&#123;</div><div class="line"> <span class="keyword">return</span> a + b;</div><div class="line">&#125;</div><div class="line"></div><div class="line">&gt;&gt; Add(<span class="number">3</span>, <span class="number">4</span>)</div><div class="line">&gt;&gt; <span class="number">7</span></div><div class="line"></div><div class="line">&gt;&gt; Add(<span class="number">5</span>, <span class="number">5</span>)</div><div class="line">&gt;&gt; <span class="number">10</span></div></pre></td></tr></table></figure> <p>这段代码定义了 <code>Add</code> 函数,接下来的 <code>&gt;&gt;</code> 符号表示对 <code>Add(3, 4)</code> 进行求值,再下一行的 <code>&gt;&gt; 7</code> 表示上一行的求值结果,不同的求值用换行分开。可以把这里的 <code>&gt;&gt;</code> 理解成控制台提示符(即Terminal中的PS)。</p> <h2 id="什么是解释器"><a href="#什么是解释器" class="headerlink" title="什么是解释器"></a>什么是解释器</h2><p><img src="http://i.imgur.com/C8lxHfr.jpg" alt="解释器图示"></p> <p><a href="http://zh.wikipedia.org/wiki/%E8%A7%A3%E9%87%8A%E5%99%A8">解释器</a>(Interpreter)是一种程序,能够读入程序并直接输出结果,如上图。相对于<a href="http://zh.wikipedia.org/wiki/%E7%BC%96%E8%AF%91%E5%99%A8">编译器</a>(Compiler),<a href="http://zh.wikipedia.org/wiki/%E8%A7%A3%E9%87%8A%E5%99%A8">解释器</a>并不会生成目标机器代码,而是直接运行源程序,简单来说:</p> <blockquote> <p><a href="http://zh.wikipedia.org/wiki/%E8%A7%A3%E9%87%8A%E5%99%A8">解释器</a>是运行程序的程序。</p> </blockquote> 如何阅读书籍 http://lucida.me//blog/on-reading-books/ 2014-03-15T20:05:35.000Z 2018-07-10T05:55:24.987Z 摘要这篇文章从如何阅读书籍出发,简单讨论了如何选择书籍、是否阅读原版和阅读数量这几个常见问题,然后自己的阅读问题进行了分析和总结。注意“如何阅读” 指 “What to read” 而非 “How to read”,Mortimer J. Adle r的 怎样阅读一本书 对How to read有着精彩的描述。“书籍”指非小说(Non-fiction)类书籍。目标我是一个功利主义者(Utilitarianism),因此我认为阅读的目标在于为自己创造实际价值,所以:我不会因为某本书看起来很有趣就去阅读(机会成本)。也不会因为很多人推荐某本书就去阅读(从众)。更不会因为某本书难就去阅读(追求智商优越感)一本书值得阅读,当且仅当:它可以直接为我创造价值。它可以间接为我创造价值。我的阅读目标:形成T型知识结构:专业知识尽可能深入,专业周边知识尽可能精炼。如何选择?专业书籍专业知识尽可能深入。我是一个软件开发者(Software Developer),因此这里的专业书籍均和软件开发有关。这里介绍我自己用的两种方法:根据引用列表从一本经典书籍出发,深度优先遍历它的引用列表,通过书评和摘要了解这些引用书籍,再根据自己的实际情况决定自己的阅读次序。这里以 代码大全 为例(为了方便和一致性,这里使用英文书名):123456789101112131415161718192021222324252627Code Complete:软件构建全程最佳实践指南。||----How to Solve it:系统解决问题。||----Conceptual Blockbusting:跳出思维的壁垒。||----Mythical Man Month:软件工程不能做什么。||----Programming Pearls:极简算法手册。 | |----The Science of Programming:编写正确的程序。 | |----Writing Efficient Programs:编写高效的程序。||----Pragmatic Programmer:高效程序员的实践。||----Refactoring:如何改进自己的代码。||----Programming on Purposes:用正确的编程模式处理问题。||----Software Tools:用合适的抽象封装复杂度。 | |----The Practice of Programming:极简编程风格指南。 | |---- Writing Solid Code:减少调试的时间。 | |---- Elements of Programming Style:极简编程风格指南。可以发现,通过 代码大全 一本书,经过短短两层引用联系,几乎可以找到2004年以前所有软件开发的经典书籍。事实上,我阅读的80%以上的软件开发经典书籍,都源自于 代码大全 的引用列表。这种方法的好处:简单直接:相对于从茫茫书海里找出10本经典书籍,找1本经典书籍再从它的引用列表里面找到20本经典书籍要容易的多;质量保证:靠谱书籍的引用书籍的质量一般都很高;发现一些被忽视的经典:相当一部分的书籍随着时间的流逝而淡出人们的视野,但这并不代表它们本身没有价值,例如:Programming on PurposesSoftware ToolsThe Science of ProgrammingWriting Solid CodeWriting Efficient Programs等等… 这些书或者绝版,但它们都对我的软件开发理念产生了巨大影响。形成知识体系:引用书籍彼此具有天然的联系,这使得创建知识体系更加容易。我认为这种方法适用于任何需要严肃阅读的领域:锚点:找到一本经典书籍。撒网:了解该书引用列表中的书籍。收网:根据自己实际需要,精读相关书籍。根据作者这里以计算机书籍为例(以下仅代表个人口味):Richard Stevens:善。Brian Kernighan:极善。Deitel Series:翔。Bruce Eckel:废话连篇。Jon Bentley:善。Andrew S Tanenbaum:大善。Jeffrey D Ullman:善。P.J. Plauger:大善。Robert C Martin:善。Bjarne Stroustrup:善,但略神叨(神侃世界观方法论有点顶不住)。Martin Fowler:善,但略唠叨。Ron Jeffries:翔(好吧我是故意来黑的,尼玛连个Sudoku都解不出来写毛程序)这种方法的问题在于需要一定阅读经验,但是它非常有效——以至于不用看内容就对书的质量有七八成把握。非本专业书籍专业周边知识尽可能精炼。对于专业周边知识,了解关键概念及指导思想即可。不需要,也没有必要对专业周边知识进行深入了解。“Know what” is enough, “Know how” is expensive.以我2年前编写手机应用,学习用户体验为例:分别在现实中(身边有几个很不错的交互设计师)和线上(Quora和知乎)进行提问和搜索,得到一个书单。按照下面的原则过滤书单:去掉教科书和大部头。去掉包含大量原理或论证的书籍。保留结论型书籍。保留指南型书籍。总结出书单,迅速的阅读并找到关键点。给大家看的设计书:CRAP原则,字体与配色。设计心理学:心智模型,心智摩擦,最小惊讶。交互设计之路:为什么需要交互,交互有哪些坑。Tapworthy:具有实际操作性的移动平台交互设计指南。了解设计的人可能认为上面的书单过于初级——没错,它们都是结论型或指南型书籍,没有原理,也没有论证——但这正是对于我这样的非专业者所需要的书籍:我不需要知道这些知识是怎么来的,知道怎么用足矣。此外,受价值驱动,而非兴趣——大多数情况下兴趣只是把自己脱离当前困境的接口。学习型书籍学习型书籍是一种元(Meta)方法书籍:这类书籍用于提升学习能力,换句话说,就是缩短吸收知识所需要的时间。这类书籍我只读过下面的几本,效果有但不明显:学习之道:冥想,体会。如何阅读一本书:检视阅读,主题阅读。Learn more, study less:建立知识体系及联系。需要注意的是,不要陷入到寻求最优学习方法的误区——Best is the worthest enemy of better。阅读原版?如何在翻译版和原版做选择?优先选择翻译版。计算机书籍这种描述精确知识的书籍更是如此。此外,如果阅读中出现难以理解的问题,不要下意识的把其归咎于翻译问题——多数情况是理解问题。为什么还有那么多人阅读原版?因为翻译版还没出版。知识的价值有其时效性。逼格。越多越好?我经常逛豆瓣,豆瓣有一个很有意思的现象就是人们喜欢去比较自己每年读书的数量,或者是截图炫耀自己读过几千本书云云。我在这里酸一下:书的数量并没有什么参考价值,就好比无法用盖一栋大楼的砖数评价这栋大楼的质量;换个说法,Effort 不等于 Progress。关键在于读过书的质量,吸收的程度,以及创造的价值。此外,盲目追求读书的数量会带来另一个问题——浅尝辄止。本应花在专业书籍上的时间被分配到其它无关紧要的事情上,导致该学好的没学好,没必要的学了一滩但用不上。总结形成 T 型知识结构:专业知识尽可能深入,专业周边知识尽可能精炼。按照引用列表和作者深入阅读专业书籍。利用结论型/指南型书籍精炼阅读专业周边书籍。不断强化自己的按需学习能力。不一定非要阅读原版。读书并非多多益善。读书之前回答下面几个问题:这本书能给自己带来什么改变?自己是否需要这种改变?如果均为 Yes,继续;如果有一个 No,砍掉。以上。]]> <h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>这篇文章从如何阅读书籍出发,简单讨论了如何选择书籍、是否阅读原版和阅读数量这几个常见问题,然后自己的阅读问题进行了分析和总结。</p> <h2 id="注意"><a href="#注意" class="headerlink" title="注意"></a>注意</h2><ol> <li>“如何阅读” 指 “What to read” 而非 “How to read”,Mortimer J. Adle r的 <a href="http://book.douban.com/subject/1013208/">怎样阅读一本书</a> 对How to read有着精彩的描述。</li> <li>“书籍”指非小说(Non-fiction)类书籍。</li> </ol> <h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><p>我是一个功利主义者(<a href="http://en.wikipedia.org/wiki/Utilitarianism">Utilitarianism</a>),因此我认为阅读的目标在于为自己创造实际价值,所以:</p> <ol> <li>我不会因为某本书看起来很有趣就去阅读(机会成本)。</li> <li>也不会因为很多人推荐某本书就去阅读(从众)。</li> <li>更不会因为某本书难就去阅读(追求智商优越感)</li> </ol> <p>一本书值得阅读,当且仅当:</p> <ol> <li>它可以直接为我创造价值。</li> <li>它可以间接为我创造价值。</li> </ol> <p>我的阅读目标:</p> <blockquote> <p>形成T型知识结构:专业知识尽可能深入,专业周边知识尽可能精炼。</p> </blockquote>