ag线上赌博:如何閱讀ag线上赌博書籍 BE DISCIPLINED 2019-10-21T05:00:04.747Z / lucida Hexo Batman vs Superman Dawn of Justice /blog/bvs/ 2019-10-20T21:45:17.000Z 2019-10-21T05:00:04.747Z (試著發一篇 post 看看這個網站還能不能用)

周末和 LP 重新看了一遍 BVS (蝙蝠俠戰超人),之前在電影院裏面看的莫名其妙,感覺 Zack 這位拍出 300 和 Watchmen 的導演怎麽搞出這部電影。這次看了導演終極剪輯版才發現這片子還可以,院線版感覺 5 分,導演剪輯版差不多 7.5 分。

院線版很多東西沒有說明白,比如 Lex 誣陷大超的這條線被剪了太多內容:被推下地鐵的黑大姐(作偽證指責大超救女友時誤殺其家人),鑲鉛的輪椅炸彈(解釋了大超為什麽沒有發現聽證會上的炸彈)。此外 Lex 拱火大超和老爺的線也被剪了不少,以至於最後 BVS 時感覺莫名其妙。導演剪輯版就好了很多,情節至少說的通。

然而導演剪輯版足足有 182 分鐘,而且還是 R 級,沒法上映也合理,只可惜一部還不錯的電影被太監了。

]]>
<p>(試著發一篇 post 看看這個網站還能不能用)</p> <p>周末和 LP 重新看了一遍 BVS (蝙蝠俠戰超人),之前在電影院裏面看的莫名其妙,感覺 Zack 這位拍出 300 和 Watchmen 的導演怎麽搞出這部電影。這次看了導演終極剪輯版才發現這片子還可以,院線
攝影手記 1 /blog/photolog-1/ 2018-07-06T23:29:59.000Z 2019-10-21T05:00:04.747Z 從去年年中間開始,機緣巧合下搞了一臺還算不錯的相機(SONY RX100V),本來是打算買來給 LP 拍照玩的,但 LP 嫌學習拍照麻煩於是我就自己先學了一遍然後再教老婆,沒想到學習的過程中對拍照產生了興趣。於是又搞了一臺可換鏡頭相機(Interchangable Lens Camera)富士 X-T20 和幾個變焦鏡頭,然後在周末一邊給 LP 拍照一邊學習拍照,到現在拍了快一萬張了。

然而這些照片都躺在硬盤裏,有時 LP 會拿這些照片發朋友圈,但我總覺得哪裏不對勁:1. 朋友圈並不是一個分享照片的好地方;2. 拍了大量的照片卻沒有反思當時拍這些照片的心路歷程以至於水平一直上不去;3. 照片和文字在一起才能形成故事(Story),加上 4. 博客吃灰。我決定寫一個不限長的攝影手記連載,記錄我拍下的照片,以及拍照片前後的故事。

Shanghai Museum

設備

在開篇簡單聊一下我的設備:

相機:

  • Fujifilm X-T20:操控很好的中價位可換鏡頭無反相機,畫質不錯,體積也很小,操控極好,適合喜歡攝影的人。
  • Sony RX100 V:便攜相機之王,追焦連拍能力超強。

鏡頭:

  • XF1024, XF1855, XF18135, 和 XF55200
  • 覆蓋 15-300 等效 35mm 焦段,從超廣角到長焦都有覆蓋
  • 都不是特別快(Fast Aperture)的鏡頭,但都夠用
  • 都帶有光學防抖,所以可以把快門拉慢 3 到 5 檔
  • 都是變焦鏡頭,因為定焦鏡頭太燒錢且玄學成分太多
  • 都不是長槍大炮,不然就失去了無反相機便攜的優勢,此外我既不需要打鳥,也不用大 Bokeh

配件:

  • 4 塊配電
  • Sandisk 高速 64GB SD 卡兩張
  • Peak Design 出品的 Cuff 和 Capture
  • Gorilla 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)閱讀筆記 /blog/Grit-read-notes/ 2018-01-29T22:29:58.000Z 2019-10-21T05:00:04.747Z 圖書簡介

Grid Book

賓夕法尼亞大學心理學副教授 Angela Duckworth 通過對成功人士進行研究,得出“堅毅”是成功人士所具備的關鍵素質,並通過其對堅毅的研究獲得麥克阿瑟天才獎。Grit (中文版 堅毅:釋放激情與堅持的力量) 這本書記錄了她的研究成果:

  1. 堅毅▓和成功存在正向相關
  2. 什麽是堅毅
  3. 如何培養堅毅

亮點

  1. 如何測量自己的堅毅值
  2. 天賦 X 努力 = 技能;技能 X 努力 = 成就
  3. 打通了 刻意練習心流成長心態 這三個概念之間的聯系
  4. 如何建立長期(高級)目標,並通過長期目標培養堅毅
  5. 如何培養激情
  6. 如何教育孩子培養堅毅(我沒有讀這部分,暫時用不到)

章節簡介

  1. 堅毅值能夠準確的預測西點軍校新生是否能堅持通過野獸訓練營
  2. 成功不僅僅需要天賦
    • 人們偏好天才
    • 努力也很重要
  3. 天賦 X 努力 = 技能;技能 X 努力 = 成就
  4. 如何測試堅毅指數
    • 堅毅指在很長的一段時間持續追求同一個頂級目標
    • 設定頂級目標,然後將中低級目標與其相連
  5. 堅毅可以通過後天塑造
    • 興趣,練習,目的,希望
  6. 對一件事擁有持久的興趣,以培養激情
  7. 通過刻意練習,獲得心流體驗
  8. 追求內心使命的召喚,而非感官享樂
  9. 學會應對失敗,提高對痛苦的抗性

語錄

決心和方向:

不管身處哪個領域,高成就者都懷有一種相當驚人的決心:

  1. 更多的韌性與勤奮
  2. 明確的知道自己要什麽

優異表現的由來:

優異的表現是幾十個技能或小活動的匯聚,這些技能或活動是習得的或偶然悟到的,經過認真錘煉,成為習慣,然後且合在一起成為一個綜合的整體

對天才的崇拜:

我們的虛榮和自戀促成了對天才的崇拜。因為如果我們認為才能是一種神奇的東西,我們就沒有必要與他人相比較,從而發現自己的不足…… 成某人有天分的意思是,你沒有必要與他競爭

天賦,技能,努力

天賦:當你投入努力的時候,你的技能能提升多快
努力:當你努力運用技能時所產生的結果

堅毅

堅毅指在很長的一段時間持續追求同一個頂級目標
堅毅可以把非睡眠時間組織起來
堅毅的人的中低級目標會與頂級目標相連

激情

激情不會頓悟,而是需要積極地去發展

刻意練習

  1. 定義清晰的延展性目標
  2. 全神貫註及不懈努力
  3. 即時的、有益的反饋
  4. 持續的反思和完善

培養目標感

  1. 反思自己,如何對社會做出積極的狀態
  2. 采取微小但有意義的方式,改變當前工作態度,使其與自己的核心價值觀> 更為緊密的聯結
  3. 從有目標感的楷模身上尋找激勵

總結

同暢銷概念書類似,這本書的套路還是:

  1. 堅毅很有用。為什麽有用?因為案例 A, B, C 證明堅毅很有用
  2. 堅毅是遺傳的嗎?不全是,可以培養
  3. 怎麽培養?通過 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) 杠鈴 /blog/2017-learned-barbell/ 2018-01-19T22:30:00.000Z 2019-10-21T05:00:04.739Z 從 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),增加力量。

選擇杠鈴

在杠鈴之前,我先後嘗試過:

  1. 啞鈴
  2. 壺鈴
  3. 彈力帶
  4. 力量操
  5. 單雙杠訓練

但效果並不理想,這些▓訓練提升了我的力量,但並沒有提升多少。我在 16 年全年都在練啞鈴,然而我的啞鈴臥推還是 130 磅,二頭肌彎舉還是 30 磅。(均為 1 次最大重量)蛋白粉也吃了不少,然而一年過去力量毫無變化。於是我開始嘗試杠鈴。

之所以一直沒有練杠鈴,是因為杠鈴太占地,而公司的健身房人又太多,經常找不到位置。但考慮到已經沒有別的選擇,加上在 Youtube 上看健身的視頻時發現他們對杠鈴都是推崇備至(尤其是 Powerlifter),於是不太情願的開始學習杠鈴。

學習杠鈴

下面是我學習杠鈴的途徑:

  1. 觀看 Youtube 的教學視頻
  2. 向公司健身房的私教學習基礎動作(硬拉,深蹲,臥推,推舉,力量翻)
    2.1. 這裏要贊一下 Google 北京的健身房教練,他在國家舉重隊實習過,非常了解綱領
  3. 從書中學習如何使用杠鈴,參考書籍是 Starting StrengthPractical Programming for Strength Training,前者用來學習動作,後者用來制定訓練計劃
  4. 每周大重量練習三次,小重量天天練習

練習計劃

每周一,三,五練習,練習計劃非常簡單:

  • 訓練 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 磅的自己:

Press

和我一起訓練的 LP,腹肌已經若隱若現(以及今年購入的 Rogue 全套設備,以後▓就可以在家練習了)

Training

]]>
<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,從對自己誠實開始 /blog/2018-being-honest/ 2018-01-07T22:47:42.000Z 2019-10-21T05:00:04.739Z 警告:意識流文章。

很久沒有寫過新文章了,無論是 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 業務不善,大量高管▓離職或是換崗,這時我意識到我有兩個選擇:

  1. 換組,在 Google 繼續渾渾噩噩的做下去。穩定,因為只要沒有大錯,Google 不會裁人。
  2. 換公司。不穩定,可能會面臨沒工作的境地。

我有想過選擇 1,但是內心的矛盾,以及在 Google 同事▓面前的尷尬,最終讓我選擇 2。與其在 Google 受人鄙視的工作下去,我寧可在其它公司,重新開始。

於是我開始了找工作的歷程,在同事朋▓友的推薦下,2017 年我進入了 FB,在 Google 渾渾噩噩的工作 3 年之後,我按下了重啟按鈕。也許這是一個錯誤的選擇,但這至少是我自己的選擇。

2017 年整年我既沒有寫新文章,也極少更新微博狀態:

  1. 相比人浮於事的 Google,FB 顯得務實很多——至少不會讓你沒有工作可做,所以我沒有那麽多時間寫文章
  2. 我不想再通過寫博客獲取關註度,來掩蓋自己在現實中落魄的事實

2017 這一年中,我在 FB 寫了不少東西,也結識了不少新的朋友。讀了一些書,也獲得了一些新的技能(開車,攝影,舉重)。最大的收獲,是終於可以誠實的面對自己,不會因為自己在線上和線下的不一致而產生自我認知矛盾。Ray Dalio 在 The Principles 一書中提到要對自己誠實,要極度現實,要保持頭腦極度開放。於是我寫下這篇文章,希望它可以:

  1. 化解我▓自己從 2014 年以來的自我認知矛盾
  2. 對自己過▓去 3 年(2014-2016)失敗▓的工作經歷做一個總結
  3. 總結 2017 年

與其在網絡上打扮成自己希望別人所看到的自己,不如對自己誠實,在現實中成為自己希望的自己。希望這篇文章可以終結我自己的浮誇,終結我 5 年前寫的那篇文章的“詛咒”。Stay hungry, stay foolish, and be honest to myself。這是 18 年的第一篇文章,但不會是最後一篇。希望我可▓以通過寫作,鍛煉自己的思考能力,總結自己的收獲,清晰自己的目標,讓自己對自己更誠實。

]]>
<p>警告:意識流文章。</p> <p>很久沒有寫過新文章了,無論是 <a href="/">lucida.me</a> 還是 <a href="/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="/">lucida</a>,已經過去了十一年,自己也從當年虎逼哄哄的小本科生變成了現在的中年油膩男子。現在回看以前自己的博客,悲哀的發現自己已遠沒有當年對技術的熱情,或是對人生目標的清晰——上學時自己的想法很單純——寫最好的程序,然後以此作為基石找到一份好工作。現在看這個目標是實現了:找到了一個對應屆生而言還不錯的工作,自己還寫了一篇現在看起來十分可笑的 <a href="http://www.cnblogs.com/figure9/archive/2013/01/09/2853649.html">心路歷程</a>,當時那篇文章被各種轉發,自己的虛榮心也得到了極大滿足。</p>
深入理解Java 8 Lambda(類庫篇——Streams API,Collectors和並行) /blog/java-8-lambdas-inside-out-library-features/ 2016-09-27T21:39:29.000Z 2019-10-21T05:00:04.747Z 關於
  1. 深入理解 Java 8 Lambda(語言篇——lambda,方法引用,目標類型和默認方法)
  2. 深入理解 Java 8 Lambda(類庫篇——Streams API,Collector 和並行)
  3. 深入理解 Java 8 Lambda(原理篇——Java 編譯器如何處理 lambda)

本文是深入理解 Java 8 Lambda 系列的第二篇,主要介紹 Java 8 針對新增語言特性而新增的類庫(例如 Streams API、Collectors 和並行)。

本文是對 Brian GoetzState 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:

  • 為現有的接口(例如 CollectionListStream)增加擴展方法;
  • 在類庫中增加新的 (stream,即 java.util.stream.Stream)抽象以便進行聚集(aggregation)操作;
  • 改造現有的類型▓使之可以提供流視圖(stream view);
  • 改造現有的類型使之可以容易的使用新的編程模式,這樣用戶就不必拋棄使用以久的類庫,例如 ArrayListHashMap(當然這並█不是說集合 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 接口,從而使得用戶可以依次遍歷集合的元素。比如我們需要把一個集合中的形狀都設置成紅色,那麽可以這麽寫:

1
2
3
for (Shape shape : shapes) {
shape.setColor(RED);
}

這個例子演示了外部叠代:for-each 循環調用 shapesiterator() 方法進行依次遍歷。外部循環的代碼非常直接,但它有如下問題:

  • Java 的 for 循環是串行的,而且必須按照集合中元素的順序進行依次處理;
  • 集合框架無法對控制流進行優化,例如通█過排序、並行、短路(short-circuiting)求值以及惰性求值改善性能。

盡管有時 for-each 循環的這些特性(串行,依次)是我們所期待的,但它對改善性能造成了阻礙。

我們可▓以使用 內部叠代(internal iteration)替代外部叠代,用戶把對叠代的控制權交給類庫,並向類庫傳遞叠代時所需執行的代碼。

下面是前例的內部叠代代碼:

1
shapes.forEach(s -> s.setColor(RED));

盡管看起來只是一個小小的語法改動,但是它們的實際差別非常巨大。用戶把對操作的控制權交還給類庫,從而允許類庫進行各種各樣的優化(例如亂序執行、惰性求值和並行等等)。總的來說,內部叠代使得外部叠代中不可能實現的優化成為可能。

外部叠代同時承擔了 做什麽(把形狀設為紅色)和 怎麽做(得到 Iterator 實例然後依次遍歷)兩項職責,而內部叠代只負責 做什麽,而把 怎麽做 留給類庫。通過這樣的職責轉變:用戶的代碼會變得更加清晰,而類庫則可以進行各種優化,從而使所有用戶都從中受益。

流(Stream)

是 Java SE 8 類庫中新增的關鍵抽象,它被定義▓於 java.util.stream(這個包裏有若幹流類型:Stream<T> 代表對象引用▓流,此外還有一系列特化(specialization)流,比如 IntStream 代表整形數字流)。每個流代表一個值序列,流提供一系列常用的聚集操作,使得我們可以便捷的在它上面進行各種運算。集合類庫也提供了便捷的方式使我們可以以操作流的方式使用▓集合、數組以及其它數據結構。

流的操作可以被組合成 流水線(Pipeline)。以前面的例子為例,如果我們只想把藍色改成紅色:

1
2
3
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.forEach(s -> s.setColor(RED));

Collection 上調用 stream() 會生成該集合元素的流視圖(stream view),接下來 filter() 操作會產生只包含藍色形狀的流,最後,這些藍色形狀會被 forEach 操作設為紅色。

如果我們想把藍色的形狀提取到新的 List 裏,則可以:

1
2
3
4
List<Shape> blue =
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.collect(Collectors.toList());

collect() 操作會把其接收的元素聚集(aggregate)到一起(這裏是 List),collect() 方法的參數則被用來指定如何進行聚集操作。在這裏我們使用 toList() 以把元素輸出到 List 中。(如需更多 collect() 方法的細節,請閱讀 Collectors 一節)

如果每個形狀都被保存在 Box 裏,然後我們想知道哪個盒子至少包含一個藍色形狀,我們可以這麽寫:

1
2
3
4
5
Set<Box> hasBlueShape =
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.map(s -> s.getContainingBox())
.collect(Collectors.toSet());

map() 操作通過映射函數(這裏的映射函數接收一個形狀,然後返回包含它的盒子)對輸入流裏面▓的元素進行依次轉換,然後產生新流。

如果我們需要得到藍色物體的▓總重量,我們可以這樣表達:

1
2
3
4
5
int 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中的 BufferedReaderRandomBitSet 已經被改造可以用做流的數據源,Arrays.stream() 則產生給定數組的流視圖。事實上,任何可以用 Iterator 描述的對象都可以成為流的數據源,如果有額外的信息(比如大小、是否有序等特性),庫還可以進行進一步的優化。

惰性(Laziness)

過濾和映射這樣的操作既可以被 急性求值(以 filter 為例,急性求值需要在方法返回前完成對所有元素的過濾),也可以被 惰性求值(用 Stream 代表過濾結果,當且僅當需要時才進行過濾操作)在實際中進行惰性運算可以帶來很多好處。比如說,如果我們█進行惰性過濾,我們就可以把過濾和流水線裏的其它操作混合在一起,從而不需要對█數據進行多遍遍歷。相類似的,如果我們在一個大型集合裏搜索第一個滿足某個條件的元素,我們可以在找到後直接停止,而不是繼續處理整個集合。(這一點對無限數據源是很重要,惰性求值對於有限數據源起到的是優化作用,但對無限數據源起到的是決定作用,沒有惰█性求值,對無限數據源的操作將無法終止)

對於過濾和映射這樣的█操作,我們很自然的會把它當成是惰性求值操作,不過它們是否真的是惰性取決於它們的具體實現。另外,像 sum() 這樣生成值的操作和 forEach() 這樣產生副█作用的操作都是“天▓然急性求值”,因為它們必須要產生具體的結果。

以下面的流水線為例:

1
2
3
4
5
int sum =
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())
.sum();

這裏的過濾操作和映射操作是惰性的,這意味著在調用 sum() 之前,我們不會從數█據源提取任何元素。在 sum 操作開始之後,我們把過濾、映射以及求和混合在對數據源的一遍遍歷之中。這樣可以大大█減少維持中間結果所帶來的開銷。

大多數循環都可以用數據源(數組、集合、生成函數以及I/O管道)上的聚合操作來表示:進行一系列惰性操作(過濾和映射等操作),然後用一個急性求值操作(forEachtoArraycollect 等操作)得到最終結果——例如過濾—映射—累積,過濾—映射—排序—遍歷等組合操作。惰性操作一般被用來計算中間結果,這在Streams API設計中得到了很好的體現——與其讓 filtermap 返回一個集合,我們選擇讓它█們返回一個新的流。在 Streams API 中,返回流對象的操作都是惰性操作,而返回非流對象的操作(或者無返回值的操作,例如 forEach())都是急性操作。絕大多數情況下,潛在的惰性█操作會被用於聚合,這正是我們想要的——流水線中的每一輪操作都會接收輸入流中的元素,進行轉換,然後把轉換結果傳給下一輪操作。

在使用這種 數據源—惰性操作—惰性操作—急性操作 流水線時,流水線中的惰性幾乎是不可見的,因為計算過程被夾在數據源和最終結果(或副作用操作)之間。這使得API的可用性和性能得到了改善。

對於 anyMatch(Predicate)findFirst() 這些急性求值操作,我們可以使用短路(short-circuiting)來終止不必要的運算。以下面的流水線為例:

1
2
3
4
Optional<Shape> firstBlue =
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.findFirst();

由於過濾這一步是惰性的,findFirst 在從其上遊得到一個元素之後就會終止,這意味著我們只會處理這個元素及其之前的元素,而不是所有元素。findFirst() 方法返回 Optional 對象,因為集合中有可能不存在滿足條件的元素。Optional 是一種用於描述可缺失值的類型。

在這種設計下,用戶並不需要顯式進行惰性求值,甚至他們都不需要了解惰性求值。類庫自己會選█擇最優化的計算方式。

並行(Parallelism)

流水線既可以串行執行也可以並行執行,並行或串行是流的屬性。除非你顯式要求使用並行流,否則JDK總會返回串行流。(串行流可以通過 parallel() 方法被轉化為並行流)

盡管並行是顯式的,但它並不需要成為侵入式的。利用 parallelStream(),我們可以輕松的把之前重量求和的代碼並行化:

1
2
3
4
5
int sum =
shapes.parallelStream()
.filter(s -> s.getColor = BLUE)
.mapToInt(s -> s.getWeight())
.sum();

並行化之後和之前的代碼區別並不大,然而我們可以█很容易看出它是並行的(此外我們並不需要自己去實現並行代碼)。

因為流的數據█源可能是一個可變集合,如果在遍歷流時數據源被修改,就會產生幹擾(interference)。所以在進█行流操作時,流的█數據源應保持不變(held constant)。這個條件並不難維持,如果集合只屬於當前線程,只要 lambda 表達式不修改流的數據源就可以。(這個條件和遍█歷集合時所需的條件相似,如果集合在遍歷時被修改,絕大多數的集合實現都會拋出ConcurrentModificationException)我們把這個條件稱為無幹擾性(non-interference)。

我們應避免在傳遞給流方法的 lambda 產生副作用。一般來說,打印調試語句這種輸出變量的操作是安全的,然而在 lambda 表達式裏訪問可變變量就有可能造成數據競爭或█是其它意想不到的問題,因為 lambda 在執行時可能會同時運行在多個線程上,因而它們所看到的元素有可能和正常的順序不一致。無幹擾性有兩層含義:

  1. 不要幹擾數據源;
  2. 不要幹擾其它 lambda 表達式,當一個 lambda 在修改某個可變狀態而█另一個 lambda 在讀取該狀態時就會產生這種幹擾。

只要滿足無幹擾性,我們就可以安全的進行並行操作並得到可預測的結果,即便對線程不安全的集合(例如 ArrayList)也是一樣。

實例(Examples)

下面的代碼源自 JDK 中的 Class 類型(getEnclosingMethod 方法),這段代碼會遍歷所有聲明的方法,然後▓根據方法名稱、返回類型以及參數的數量和類型進行匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for (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, check return type
if (method.getReturnType().equals(returnType)) {
return method;
}
}
}
}
}
throw new InternalError("Enclosing method not found");

通過█使用流,我們不但可以消除上面代碼裏面所有的臨時變量,還可以把控制邏輯交給類庫處理。通過反射得到方法列表之後,我們利用 Arrays.stream 將它轉化為 Stream,然後利用一系列過濾器去除類型不符、參數不符以及返回值不符的方法,然後通過調用 findFirst 得到 Optional<Method>,最後利用 orElseThrow 返回目標值或者拋出異常。

1
2
3
4
5
6
return 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"));

相對於未使用流的代碼,這段代█碼更加緊湊,可讀性更好,也不容易出錯。

流操作特別適合對集合進行查詢操作。假設有一個“音樂庫”應用,這個應用裏每個庫都有一個專輯列表,每張專輯都有其名稱和音軌列表,每首音軌█表都有名稱、藝術家和評分。

假設我們需要得到一個按名█字排序的專輯列表,專輯列表裏面的每張專輯都至少包含一首四星及四星以上的音軌,為了構建這個專輯列表,我們可以這麽寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Album> favs = new ArrayList<>();
for (Album album : 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, Album a2) {
return a1.name.compareTo(a2.name);
}
});

我們可以用流操作來完成上面代碼中的三█個主要步驟——識別一張專輯是否包含一首評分大於等於四星的音軌(使用 anyMatch);按名字排序;以及把滿足條件的專輯放在一個 List 中:

1
2
3
4
5
List<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() 方法把流中的元素聚合到 ListSet 中。collect() 接收一個類型為 Collector 的參數,這個參數決定了如何把流中的元素聚合到其它數據結構中。Collectors 類包含了大量常用收集器的工廠方法,toList()toSet() 就是其中最常見的兩個,除了它們還有很多收集器,用來對數據進行對復雜的轉換。

Collector 的類型由其輸入類型和輸出類型決定。以 toList() 收集器為例,它的輸入類型為 T,輸出類型為 List<T>toMap 是另外一個較為復雜的 Collector,它有若幹個版本。最簡單的版本接收一對函數作為輸入,其中一個函數用來生成鍵(key),另一個函數用來生成值(value)。toMap 的輸入類型是 T,輸出類型是 Map<K, V>,其中 KV 分別是前面兩個函數所生成的鍵類型和值類型。(復雜版本的 toMap 收集器則允許你指定目標 Map 的類▓型或解決鍵沖突)。舉例來說,下面的代碼以目錄數字為鍵值創建一個倒排索引:

1
2
3
Map<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 的鍵是分類函數的返回結果,值是該分類下的元素列表。

1
2
3
4
Map<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 收集器:

1
2
3
4
5
Map<Artist, Set<Track>> favsByArtist =
tracks.stream()
.filter(t -> t.rating >= 4)
.collect(Collectors.groupingBy(t -> t.artist,
Collectors.toSet()));

如果我們需要按照歌手和評分來管理歌曲,我們可以生成多級 Map

1
2
3
4
Map<Artist, Map<Integer, List<Track>>> byArtistAndRating =
tracks.stream()
.collect(groupingBy(t -> t.artist,
groupingBy(t -> t.rating)));

在最後的例子裏,我們創建了一個歌曲標題裏面的詞頻分布。我們首先使用 Stream.flatMap() 得到一個歌曲流,然後用 Pattern.splitAsStream 把每首歌曲的標題打散成詞流;接下來我們用 groupingByString.toUpperCase 對這些詞進行不區分大小寫的分組,最後使用 counting() 收集器計算每個詞出現的次數(從而無需創建中間集合)。

1
2
3
4
5
Pattern 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 的實現原理。

為了能夠並行化任意流上的所有操作,我們把流抽象為 SpliteratorSpliterator 是對傳統叠代器概念的一個泛化。分割叠代器(spliterator)既支持順序依次訪問數據,也支持分解數據:就像 Iterator 允許你跳過一個元素然後保留剩下的元素,Spliterator 允許你把輸入元素的一部分(一般來說是一半)轉移(carve off)到另一個新的 Spliterator 中,而剩下的數據則會被保存在原來的 Spliterator 裏。(這兩個分割叠代器還可以被進一步分解)除此之外,分割叠代器還可以提供源的元數據(比如元素的數量,如果已知的話)和其它一系列布爾值特征(比如說“元素是否被排序”這樣的特征),Streams 框架可以利用這些數據來進行優化。

上面的分解方法也同樣適用於其它數據結構,數據結構的作者只需要提供分解邏輯,然後就可以直接享用並行流操作帶來的遍歷。

大多數用戶無需去實現 Spliterator 接口,因為集合上的 stream() 方法往往就足█夠了。但如果你需要實現一個集合或一個流,那麽你可能需要手動實現 Spliterator 接口。Spliterator 接口的API如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Spliterator<T> {
// Element access
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();
}

集合庫中的基礎接口 CollectionIterable 都實現了正確但相對低效的 spliterator() 實現,但派生接口(例如 Set)和具體實現類(例如 ArrayList)均提供了高效的分割叠代器實現。分割叠代器的實現質量會影響到流操作的執行效率;如果在 split() 方法中進行良好(平衡)的劃分,CPU 的利用率會得到改善;此外,提供正確的特性(characteristics)和大小(size)這些元數據有利於進一步優化。

出現順序(Encounter order)

多數數據結構(例如列表,數組和I/O通道)都擁有 自然出現順序(natural encounter order),這意味著它們的元素出現順序是可預測的。其它的數據結構(例如 HashSet)則沒有一個明確定義█的出現順序(這也是 HashSetIterator 實現中不保證元素出現順序的原因)。

是否具有明確定義的出現順序是 Spliterator 檢查的特性之一(這個特性也被流使用)。除了少數例外(比如 Stream.forEach()Stream.findAny()),並行操作一般都會受到出現順序的限制。這意味著下面的流水線:

1
2
3
4
List<String> names =
people.parallelStream()
.map(Person::getName)
.collect(toList());

代碼中名字出現的順序必須要和流中的 Person 出現的順序一致。一般來說,這是我們所期待的結果,而且它對多大多數的流實現都不會造成明顯的性能損耗。從另外的角度來說,如果源數據是 HashSet,那麽上面代碼中名字就可以以任意順序出現。

JDK 中的流和 lambda(Streams and lambdas in JDK)

Stream 在 Java SE 8 中非常重要,我們希望可以在 JDK 中盡可能廣的使用 Stream。我們為 Collection 提供了 stream()parallelStream(),以便把集合轉化為流;此外數組可以通過 Arrays.stream() 被轉化為流。

除此之外,Stream 中還有一些靜態工廠方法(以及相關的原始類型流實現),這些方法被用來創建流,例如 Stream.of()Stream.generate 以及 IntStream.range。其它的常用類型也提供了流相關的方法,例如 String.charsBufferedReader.linesPattern.splitAsStreamRandom.intsBitSet.stream

最後,我們提供了一系列API用於構建流,類庫的編寫者可以利用這些API來在流上實現其它聚集操作。實現 Stream 至少需要一個 Iterator,不過如果編寫者還擁有其它元數據(例如數據大小),類庫就可以通過 Spliterator 提供一個更加高效的實現(就像 JDK 中所有的集合一樣)。

比較器工廠(Comparator factories)

我們在 Comparator 接口中新增了若幹用於生成比較器的實用方法:

靜態方法 Comparator.comparing() 接收一個函數(該函數返回一個實現 Comparable 接口的比較鍵值),返回一個 Comparator,它的實現十分簡潔:

1
2
3
4
public 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));
}

我們把這種方法稱為 高階函數 ——以函數作為參數或是返回值的函數。我們可以使用高階函數簡化代碼:

1
2
List<Person> people = ...
people.sort(comparing(p -> p.getLastName()));

這段代碼比“過去的代碼”(一般要定義一個實現 Comparator 接口的匿名類)要簡潔很多。但是它真正的威力在於它大大改進了可組合性(composability)。舉例來說,Comparator 擁有一個用於逆序的默認方法。於是,如果想把列表按照姓進行反序排序,我們只需要創建一個和之前一樣的比較器,然後調用反序方法即可:

1
people.sort(comparing(p -> p.getLastName()).reversed());

與之類似,默認方法 thenComparing 允許你去改進一個已有的 Comparator:在原比較器返回相等的結果時進行進一步比較。下面的代碼演示了如何按照姓和名進行排序:

1
2
3
4
Comparator<Person> c =
Comparator.comparing(p -> p.getLastName())
.thenComparing(p -> p.getFirstName());
people.sort(c);

可變的集合操作(Mutative collection operation)

集合上的流操作一般會生成一個新的值或集合。不過有時我們希望就地修改集合,所以我們為集合(例如 CollectionListMap)提供了一些新的方法,比如 Iterable.forEach(Consumer)Collection.removeAll(Predicate)List.replaceAll(UnaryOperator)List.sort(Comparator)Map.computeIfAbsent()。除此之外,ConcurrentMap 中的一些非原子方法(例如 replaceputIfAbsent)被提升到 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,方法引用,目標類型和默認方法) /blog/java-8-lambdas-insideout-language-features/ 2016-09-25T20:30:02.000Z 2019-10-21T05:00:04.747Z 關於
  1. 深入理解 Java 8 Lambda(語言篇——lambda,方法引用,目標類型和默認方法)
  2. 深入理解 Java 8 Lambda(類庫篇——Streams API,Collector 和並行)
  3. 深入理解 Java 8 Lambda(原理篇——Java 編譯器如何處理 lambda)

本文是深入理解 Java 8 Lambda 系列的第一▓篇,主要介紹 Java 8 新增的語言特性(比如 lambda 和方法引用),語言概念(比如目標類型和變量捕獲)以及設計思路。

本文是對 Brian GoetzState of Lambda 一文的翻譯,那麽問題來了:

為什麽要翻譯這個系列?

  1. 工作之後,我開始大量使用 Java
  2. 公司將會在不久的未來使用 Java 8
  3. 作為資質平庸的開發者,我需要打一點提前量,以免到時拙計
  4. 為了學習Java 8(主要是其中的 lambda 及相關庫),我先後閱讀了Oracle的 官方文檔Cay HorstmannCore Java的作者)的 Java 8 for the Really Impatient 和Richard Warburton的 Java 8 Lambdas
  5. 但我感到並沒有多大收獲,Oracle的官方文檔涉及了 lambda 表達式的每一個概念,但都是點到輒止;後兩本書(尤其是Java 8 Lambdas)花了大量篇幅介紹 Java lambda 及其類庫,但實質內容不多,讀完了還是沒有對Java lambda產生一個清晰的認識
  6. 關鍵在於這些文章和書都沒有解決我對Java lambda的困惑,比如:
    • Java 8 中的 lambda 為什麽要設計成這樣?(為什麽要一個 lambda 對應一個接口?而不是 Structural Typing?)
    • lambda 和匿名類型的關系是什麽?lambda 是匿名對象的語法糖嗎?
    • Java 8 是如何對 lambda 進行類型推導的?它的類型推導做到了什麽程度?
    • Java 8 為什麽要引入默認方法?
    • Java 編譯器如何處理 lambda?
    • 等等……
  7. 之後我在 Google 搜索這些問題,然後就找到 Brian Goetz 的三篇關於Java lambda的文章(State of LambdaState of Lambda libraries versionTranslation of lambda),讀完之後上面的問題都得到了解決
  8. 為了加深理解,我決定翻譯這一系列文章

警告(Caveats)

如果你不知道什麽是函數式編程,或者不了解 mapfilterreduce 這些常用的高階函數,那麽你不適合閱讀本文,請先學習函數式編程基礎(比如 這本書)。


State of Lambda by Brian Goetz

The high-level goal of Project Lambda is to enable programming patterns that require modeling code as data to be convenient and idiomatic in Java.

關於

本文介紹了 Java SE 8 中新引入的 lambda 語言特性以及這些特性背後的設計思想。這些特性包括:

  • lambda 表達式(又被成為“閉包”或“匿名方法”)
  • 方法引用和構造方法引用▓
  • 擴展的目標類型和類型推導
  • 接口中的默認方法和靜態方法

1. 背景

Java 是一門面向對象編程語言。面向對象編程語言和函數式編程語言中的基本元素(Basic Values)都可以動態封裝程序行為:面向對象編程語言使用帶有方法的對象封裝行為,函數式編程語言使用函數封裝行為。但這個相同點並不明顯,因為Java 對象往往比較“重量級”:實例化一個類型往往會涉及不同的類,並需要初始化類裏的字段和方法。

不過有些 Java 對象只是對單個函數的封裝。例如下面這個典型用例:Java API 中定義了一個接口(一般被稱為回調接口),用戶通過提供這個接口的實例來傳入指定行為,例如:

1
2
3
public interface ActionListener {
void actionPerformed(ActionEvent e);
}

這裏並不需要專門定義一個類來實現 ActionListener,因為它只會在調用處被使用一次。用戶一般會使用匿名類型把行為內聯(inline):

1
2
3
4
5
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});

很多庫都依賴於上面的模式。對於並行 API 更是如此,因為我們需要把待執行的代碼提供給並行 API,並行編程是一個非常值得研究的領域,因為在這裏摩爾定律得到了重生:盡管我們沒有更快的 CPU 核心(core),但是我們有更多的 CPU 核心。而串行 API 就只能使用有限的計算能力。

隨著回調模式和函數式編程風格的日益流行,我們需要在Java中提供一種盡可能輕量級的將代碼封裝為數據(Model code as data)的方法。匿名內部類並不是一個好的 選擇,因為:

  1. 語法過於冗余
  2. 匿名類中的 this 和變量名容易使人產生誤解
  3. 類型載入和實例創建語義不夠靈活
  4. 無法捕獲非 final 的局部變量
  5. 無法對控制流進行抽象

上面的多數問題均在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 註解來顯式指定一個接口是函數式接口(以避免無意聲明了一個符合函數式標準的接口),加上這個註解之後,編譯器就會驗證該接口是否滿足函數式接口的要求。

實現函數式類型的另一種方式是引入一個全新的 結構化 函數類型,我們也稱其為“箭頭”類型。例如,一個接收 StringObject 並返回 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 SE 8中增加了一個新的包:java.util.function,它裏面包含了常用的函數式接口,例如:

  • Predicate<T>——接收 T 並返回 boolean
  • Consumer<T>——接收 T,不返回值
  • Function<T, R>——接收 T,返回 R
  • Supplier<T>——提供 T 對象(例如工廠),不接收值
  • UnaryOperator<T>——接收 T 對象,返回 T
  • BinaryOperator<T>——接收兩個 T,返回 T

除了上面的這些基本的函數式接口,我們還提供了一些針對原始類型(Primitive type)的特化(Specialization)函數式接口,例如 IntSupplierLongBinaryOperator。(我們只為 intlongdouble 提供了特化函數式接口,如果需要使用其它原始類型則需要進行類型轉換)同樣的我們也提供了一些針對多個參數的函數式接口,例如 BiFunction<T, U, R>,它接收 T 對象和 U 對象,返回 R 對象。

3. lambda表達式(lambda expressions)

匿名類型最大的問題就在於其冗余的語法。有人戲稱匿名類型導致了“高度問題”(height problem):比如前面 ActionListener 的例子裏的五行代碼中僅有一行在做實際工作。

lambda表達式是匿名方法,它提供了輕量級的語法,從而解決了匿名內部類帶來的“高度問題”。

下面是一些lambda表達式:

1
2
3
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }

第一個 lambda 表達式接收 xy 這兩個整形參數並返回它們的和;第二個 lambda 表達式不接收參數,返回整數 ‘42’;第三個 lambda 表達式接收一個字符串並把它打印到控制臺,不返回值。

lambda 表達式的語法由參數列表、箭頭符號 -> 和函數體組成。函數體既可以是一個表達式,也可以是一個語句塊:

  • 表達式:表達式會被執行然後返回執行結果。
  • 語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣——
    • return 語句會把控制權交給匿名方法的調用者
    • breakcontinue 只能在循環中使用
    • 如果函數體有返回值,那麽函數體內部的每一條路徑都必須返回值

表達式函數體適合小型 lambda 表達式,它消除了 return 關鍵字,使得語法更加簡潔。

lambda 表達式也會經常出現在嵌套環境中,比如說作為方法的參數。為了使 lambda 表達式在這些場景下盡可能簡潔,我們去除了不必要的分隔符。不過在某些情況下我們也可以把它分為多行,然後用括號包起來,就像其它普通表達式一樣。

下面是一些出現在語句中的 lambda 表達式:

1
2
3
4
5
6
7
8
FileFilter 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

1
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());

這就意味著同樣的 lambda 表達式在不同上下文裏可以擁有不同的類型:

1
2
3
Callable<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 表達式的參數類型可以從目標類型中得出:

1
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);

在上面的例子裏,編譯器可以推導出 s1s2 的類型是 String。此外,當 lambda 的參數只有一個而且它的類型可以被推導得知時,該參數列表外面的括號可以被省略:

1
2
3
FileFilter java = f -> f.getName().endsWith(".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));

這些改進進一步展示了我們的設計目標:“不要把高度問題轉化成寬度問題。”我們希望語法元素能夠盡可能的少,以便代碼的讀者能夠直達 lambda 表達式的核心部分。

lambda 表達式並不是第一個擁有上下文相關類型的 Java 表達式:泛型方法調用和“菱形”構造器調用也通過目標類型來進行類型推導:

1
2
3
4
5
List<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)表達式

在前三個上下文(變量聲明、賦值和返回語句)裏,目標類型即是被賦值或被返回的類型:

1
2
3
4
5
6
7
8
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
public Runnable toDoLater() {
return () -> {
System.out.println("later");
}
}

數組初▓始化器和賦值類似,只是這裏的“變量”變成了數組元素,而類型是從數組類型中推導得知:

1
2
3
4
filterFiles(
new FileFilter[] {
f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")
});

方法參數的類型推導要相對復雜些:目標類型的確認會涉及到其它兩個語言特性:重載解析(Overload resolution)和參數類型推導(Type argument inference)。

重載解析會為一個給定的方法調用(method invocation)尋找最合適的方法聲明(method declaration)。由於不同的▓聲明具有不同的簽名,當 lambda 表達式作為方法參數時,重載解析就會影響到 lambda 表達式的目標類型。編譯器會通過它所得之的信息來做出決定。如果 lambda 表達▓式具有 顯式類型(參數類型被顯式指定),編譯器就可以直接 使用lambda 表達式的返回類型;如果lambda表達式具有 隱式類型(參數類型被推導而知),重載解析則會忽略 lambda 表達式函數體而只依賴 lambda 表達式參數的數量。

如果在解析方法聲明時存在二義性(ambiguous),我們就需要利用轉型(cast)或顯式 lambda 表達式來提供更多的類型信息。如果 lambda 表達式的返回類型依賴於其參數的類型,那麽 lambda 表達式函數體有可能可以給編譯器提供額外的信息,以便其推導參數類型。

1
2
List<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 表達式可以通過外部目標類型推導出其內部的返回類型,這意味著我們可以方便的編寫一個返回函數的函數:

1
Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };

類似的,條件表達式可以把目標類型“分發”給其子表達式:

1
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);

最後,轉型表達式(Cast expression)可以顯式提供 lambda 表達式的類型,這個特性在無法確認目標類型時非常有用:

1
2
// Object o = () -> { System.out.println("hi"); }; 這段代碼是非法的
Object o = (Runnable) () -> { System.out.println("hi"); };

除此之外,當重載的方法都擁有函數式接口時,轉型可以幫助解決重載解析時出現的二義性。

目標類型這個概念不僅僅適用於 lambda 表達式,泛型方法調用和“菱形”構造方法調用也可以從目標類型中受益,下面的代碼在 Java SE 7 是非法的,但在 Java SE 8 中是合法的:

1
2
3
List<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!" 打印兩遍:

1
2
3
4
5
6
7
8
9
10
11
public class 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@5b89a773Hello$2@537a7706 之類的字符串,這往往會使開發者大吃一驚。

基於詞法作用域的理念,lambda 表達式不可以掩蓋任何其所在上下文中的局部變量,它的行為和那些擁有參數的控制流結構(例如 for 循環和 catch 從句)一致。

個人補充:這個說法很拗口,所以我在這裏加一個例子以演示詞法作用域:

1
2
3
4
5
int 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 後也不會導致編譯錯誤的局部變量就是有效只讀變量。

1
2
3
4
Callable<String> helloCallable(String name) {
String hello = "Hello";
return () -> (hello + ", " + name);
}

this 的引用,以及通過 this 對未限定字段的引用和未限定方法的調用在本質上都屬於使用 final 局部變量。包含此類引用的 lambda 表達式相當於捕獲了 this 實例。在其它情況下,lambda 對象不會保留任何對 this 的引用。

這個特性對內存管理是一件好事:內部類實例會一直保留一個對其外部類實例的強引用,而那些沒有捕獲外部類成員的 lambda 表達式則不會保留對外部類實例的引用。要知道內█部類的這個特性往往會造成內存泄露。

盡管我們█放寬了對捕獲變量的語法限制,但試圖修改捕獲變量的行為仍然會被禁止,比如下面這個例子就是非法的:

1
2
int sum = 0;
list.forEach(e -> { sum += e.size(); });

為什麽要禁止這種行為呢?因為這樣的 lambda 表達式很容易引起 race condition。除非我█們能夠強制(最好是在編譯時)這樣的函█數不能離開其當前線程,但如果這麽做了可能會導致更多的問題。簡而言之,lambda 表達式對 封閉,對 變量 開放。

個人補充:lambda 表達式對 封閉,對 變量 開放的原文是:lambda expressions close over values, not variables,我在這裏增加一個例子以說明這個特性:

1
2
3
4
5
int sum = 0;
list.forEach(e -> { sum += e.size(); }); // Illegal, close over values
List<Integer> aList = new List<>();
list.forEach(e -> { aList.add(e); }); // Legal, open over variables

lambda 表達式不支持修改捕獲變量的另一個原因是我們可以使用更好的方式來實現同樣的效果:使用規約(reduction)。java.util.stream 包提供了各種通用的和專用的規約操作(例如 summinmax),就上面的例子而言,我們可以使用規約操作(在串行█和並行下都是安全的)來代替 forEach

1
2
3
4
int sum =
list.stream()
.mapToInt(e -> e.size())
.sum();

sum() 等價於下面的規約操作:

1
2
3
4
int sum =
list.stream()
.mapToInt(e -> e.size())
.reduce(0 , (x, y) -> x + y);

規約需要一個初始值(以防輸入為空)和一個操作符(在這裏是加號),然後用下█面的表達式計算結果:

1
0 + list[0] + list[1] + list[2] + ...

規約也可以完成其它操作,比如求最小值、最大值和乘積等等。如果操作符具有可結合性(associative),那麽規約操作就可以容易的被並行化。所以,與其支持一個本質上是並行而且容易導致 race condition 的操作,我們選擇在庫中提供一個更加並行友好且不容易出錯的方式來進行累積(accumulation)。

8. 方法引用(Method references)

lambda 表達式允許我們定義一個匿名方法,並允許我們以函數式接口的方式使用它。我們也希望能夠在 已有的 方法上█實現同樣的特性。

方法引用和 lambda 表達式擁有相同的特性(例如,它們都需要一個目標類型,並需要被轉化為函數式接口的實例),不過我們並不需要為方法引用提供方法體,我們可以█直接通過方法名稱引用已有方法。

以下面的代碼為例,假設我們要按照 nameagePerson 數組進行排序:

1
2
3
4
5
6
7
8
9
10
11
12
class 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表達式:

1
Comparator<Person> byName = Comparator.comparing(Person::getName);

這裏的 Person::getName 可以被看作為 lambda 表達式的簡寫形式。盡管方法引用不一定(比如在這個例子裏)會把語法變的更緊湊,但它擁有更明確的語義——如果我們想要調用的方法擁有一個名字,我們就可█以通過它的名字直接調用它。

因為函數式接口的方法參數對應於隱式方法調用時的參數,所以被引用方法簽名可以通過放寬類型,裝箱以及組織到參數數組中的方式對其參數進行操作,就像在調用實際方法一樣:

1
2
3
4
Consumer<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

對於具體對象上的實例方法引用,我們則需要在對象名和方法名之間加入分隔符:

1
2
Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;

這裏的隱式 lambda 表達式(也就是實例方法引用)會從 knownNames 中捕獲 String 對象,而它的方法體則會通過Set.contains 使用該 String 對象。

有了█實例方法引用,在不同函數式接口之間進行類型轉換就變的很方便:

1
2
Callable<Path> c = ...
Privileged<Path> a = c::call;

引用任意對象的實例方法則需要在實例方法名稱和其所屬類型名稱間加上分隔符:

1
Function<String, String> upperfier = String::toUpperCase;

這裏的隱式 lambda 表達式(即 String::toUpperCase 實例方法引用)有一個 String 參數,這個參數會被 toUpperCase 方法使用。

如果類型的實例方法是泛型的,那麽我們就需要在 :: 分隔符前提供類型參數,或者(多數情況下)利用目標類型推導出其類型。

需要註意的是,靜態方法引用和類型上的實例方法引用擁有一樣的語法。編譯器會根據實際情況做出決定。

一般我們不需要指定方法引用中的參數類型,因為編譯器往往可以推導出結果,但如果需要我們也可以顯式在 :: 分隔符之前提供參數類型信息。

和靜態方法引用類似,構造方法也可以通過 new 關鍵字被直接引用:

1
SocketImplFactory factory = MySocketImpl::new;

如果類型擁有多個構造方法,那麽我們就會通過目標類型的方法參數來選擇最佳匹配,這裏的選█擇過程和調用構造方法時的選擇過程是一樣的。

如果待實例化的類型是泛型的,那麽我們可以在類型名稱之後提供類型參數,否則編譯器則會依照”菱形”構造方法調用時的方式進行推導。

數組的構造方法引用的語法則比較特殊,為了便於理解,你可以假想存在一個接收 int 參數的數組構造方法。參考下面的代碼:

1
2
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 創建數組 int[10]

10. 默認方法和靜態接口方法(Default and static interface methods)

lambda 表達式和方法引用大大提升了 Java 的表達能力(expressiveness),不過為了使把 代碼即數據 (code-as-data)變的更加容易,我們需要把這些特性融入到已有的庫之中,以便開發者使用▓。

Java SE 7 時代為一個已有的類庫增加功能是非常困難的。具體的說,接口在發布之後就已經被定型,除非我們能夠一次性更新所有該接口的實現,否則向接口添加方法就會破壞現有的接口實現。默認方法(之前被稱為 虛擬擴展方法守護方法)的目標即是解決這個問題,使得接口在發布之後仍能被逐步演化。

這裏給出一個例子,我們需要在標準集合▓ API 中增加針對 lambda 的方法。例如 removeAll 方法應該被泛化為接收一個函數式接口 Predicate,但這個新的方法應該被放在哪裏呢?我們無法直接在 Collection 接口上新增方法——不然就會破壞現有的 Collection 實現。我們倒是可以在 Collections 工具類中增加對應的靜態方法,但這樣就會把這個方法置於“二等公民”的境地。

默認方法 利用面向對象的方式向接口增加新的行為。它是一種新的方法:接口方法可以是 抽象的 或是 默認的。默認方法擁有其默認實現,實現接口的類型通過繼承得到該默認實現(如果類型沒有覆蓋該默認實現)。此外,默認方法不是抽象方法,所以我們可以放心的向函數式接口裏增加默認方法,而不用擔心函數式接口的單抽象方法限制。

下面的例子展示了如何向 Iterator 接口增加默認方法 skip

1
2
3
4
5
6
7
8
9
interface 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 的默認實現:調用 hasNextnext 若幹次。子類可以通過覆蓋 skip 來提供更好的實現——比如直接移動遊標(cursor),或是提供為操作提供原子性(Atomicity)等。

當接口繼承其它接口時,我們既可以為它所繼承而來的抽象方法提供一個默認實現,也可以為它繼承而來的默認方法提供一個新的實現,還可以把它繼承而來的默認方法重新抽象化。

除了默認方法,Java SE 8 還在允許在接口中定義 靜態 方法。這使得我們可以從接口直接調用和它相關的輔助方法(Helper method),而不是從其它的類中調用(之前這樣的類往往以對應接口的復數命名,例如 Collections)。比如,我們一般需要使用靜態輔助方法生成實現 Comparator 的比較器,在Java SE 8中我們可以直接把該靜態方法定義在 Comparator 接口中:

1
2
3
4
public 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)聲明優先於接口默認方法。無論該方法是具體的還是抽象的。
  • 被其它類型所覆蓋的方法會被忽略。這條規則適用於超類型共享一個公共祖先的情況。

為了演示第二條規則,我們假設 CollectionList 接口均提供了 removeAll 的默認實現,然後 Queue 繼承並覆蓋了 Collection 中的默認方法。在下面的 implement 從句中,List 中的方法聲明會優先於 Queue 中的方法聲明:

1
class LinkedList<E> implements List<E>, Queue<E> { ... }

當兩個獨立的默認方法相沖突或是默認方法和抽象方法相沖突時會產生編譯錯誤。這時程序員需要顯式覆蓋超類方法。一般來說我們會定義一個默認方法,然後在其中顯式選擇超類方法:

1
2
3
interface Robot implements Artist, Gun {
default void draw() { Artist.super.draw(); }
}

super 前面的類型必須是有定義或繼承默認方法的類型。這種方法調用並不只限於消除命名沖突——我們也可以在其它場景中使用它。

最後,接口在 inheritsextends 從句中的聲明順序和它們被實現的順序無關。

12. 融會貫通(Putting it together)

我們在設計lambda時的一個重要目標就是新增的語言特性和庫特性能夠無縫結合(designed to work together)。接下來,我們通過一個實際例子(按照姓對名字列表進行排序)來演示這一點:

比如說下面的代碼:

1
2
3
4
5
6
List<Person> people = ...
Collections.sort(people, new Comparator<Person>() {
public int compare(Person x, Person y) {
return x.getLastName().compareTo(y.getLastName());
}
})

冗余代碼實在太多了!

有了lambda表達式,我們可以去掉冗余的匿名類:

1
2
Collections.sort(
people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));

盡管代碼簡潔了很多,但它的抽象程度依然很差:開發者仍然▓需要進行實際的比較操作(而且如果比較的值是原始類型那麽情況會更糟),所以我們要借助 Comparator 裏的 comparing 方法實現比較操作:

1
Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));

在類型推導和靜態導入的幫助下,我們可以進一步簡化上面的代碼:

1
Collections.sort(people, comparing(p -> p.getLastName()));

我們註意到這裏的 lambda 表達式實際上是 getLastName 的代理(forwarder),於是我們可以用方法引用代替它:

1
Collections.sort(people, comparing(Person::getLastName));

最後,使用 Collections.sort 這樣的輔助方法並不是一個好主意:它不但使代碼變的冗余,也無法為實現 List 接口的數據結構提供特定(specialized)的高效實現,而且由於 Collections.sort 方法不屬於 List 接口,用戶在閱讀 List 接口的文檔時不會察覺在另外的 Collections 類中還有一個針對 List 接口的排序(sort())方法。

默認方法可以有效的解決這個問題,我們為 List 增加默認方法 sort(),然後就可以這樣調用:

1
people.sort(comparing(Person::getLastName));;

此外,如果▓我們為 Comparator 接口增加一個默認方法 reversed()(產生一個逆序比較器),我們就可以非常容易的在前面代碼的基礎上實現降序排序。

1
people.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 個關鍵狀態 /blog/nine-states-of-design/ 2016-01-19T19:14:22.000Z 2019-10-21T05:00:04.739Z 這篇文章介紹了 初始載入,和 等設計中的 9 個關鍵狀態。通過將這些設計狀態引入設計/開發流程,我們在設計/開發更會主動的為用戶著想,我們的產品將會更貼近用戶,從而具有更強的競爭力。

緣起

這次回國,我見到不少在創業的朋友,也把玩了他們的產品(絕大多數是手機應用),並為他們的產品提供各種建議。經過交流,我發現他們的產品有一個通病:只考慮理想狀態(happy path),而忽視其它狀態(unhappy path)。

這麽說比較抽象,我舉幾個例子:

  • 某購物推薦應用,我在註冊之後進入應用主界面,這時推薦列表是空的(因為用戶還沒有選擇任何偏好)
  • 某訂飯應用,我完成訂單後沒有任何提示,直到經過一番點擊,我才意識到已下的訂單在另外▓一個頁面裏
  • 某新聞應用,我在註冊時輸入密█碼,點擊確認,沒有任何反應,詢問應用開發者之後才直到密碼不能少於 8 位

總而言之,這些交互問題都是只考慮理想狀態而導致的。按理說這些問題在測試時就應該被發現,但為什麽沒有被發現呢?我觀察了下他們的測試流程:

  1. 安裝應用
  2. 註冊,登錄,填一些數據
  3. 各種測試

這時你可能已經發現問題了——開發者和測試者都對產品很熟悉,因此他們跳過了第 2 步(註冊,登錄,填一些數據)直接進入第 3 步進行測試,這就導致了第 2 步中的問題很難被發現。然而第 2 步至關重要,因為如果註冊有問題或是用戶不知道如何增加數據,那麽他/她很可能就不會繼續使用這個應用。

我把這個問題反映給應用的開發者/設計者,他們大多表示贊同,並詢問如何避免這些問題再度發生。我說我知道兩個方法:如果有時間,閱讀 探索性軟件測試(我的前 BOSS 給我的推薦讀物之一);如果時間不足,閱讀 Medium 上的 The Nine States of Design。下面即是 The Nine States of Design 的譯文全文:

現代的 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 設計的分頁 UI

7. 錯誤

當用戶輸入錯誤時,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 /blog/whiteboard-coding-demystified/ 2015-05-31T15:45:42.000Z 2019-10-21T05:00:04.747Z 面試很困難,技術面試更加困難——只用 45 ~ 60 分鐘是很難考察出面試者的水平的。所以 劉未鵬 在他的 怎樣花兩年時間去面試一個人 一文中鼓勵面試者創建 GitHub 賬號,閱讀技術書籍,建立技術影響力,從而提供給面試官真實,明確,可度量的經歷。

這種方法對面試者效果很好,但對面試官效果就很一般——面試官要面對大量的面試者,這些面試者之中可能只有很少人擁有技術博客,但這並不代表他們的技術能力不夠強(也許他們對寫作不感興趣);另一方面,一些人擁有技術博客,但這也不能說明他們的水平就一定會很牛(也許他們在嘴遁呢█)。

總之,技術博客和 GitHub 賬號是加分項,但技術面試仍然必不可少。所以,問題又回來了,如何進行高效的技術面試?或者說,如何在 45 ~ 60 分鐘內盡可能準確的考察出面試者的技術水平?

回答這個問題之前,讓我們先看下技術面試中的常見問題都有什麽:

技術面試中的常見問題

技術面試中的問題大致可以分為 5 類:

  1. 編碼:考察面試者的編碼能力,一般要求面試者在 20 ~ 30 分鐘之內編寫一段需求明確的小程序(例:編寫一個函數劃分一個整形數組,把負數放在左邊,零放在中間,正數放在右邊);
  2. 設計:考察面試者的設計/表達能力,一般要求面試者在 30 分鐘左右█內給出一個系統的大致設計(例:設計一個類似微博的系統)
  3. 項目:考察面試者的設計/表達能力以及其簡歷的真實度(例:描述你做過的 xxx 系統中的難點,以及你是如何克服這些難點)
  4. 腦筋急轉彎:考察面試者的『反應/智力』(例:如果你變成螞蟻大小然後被扔進一個攪拌機裏,你將如何脫身?)
  5. 查漏:考察面試者對某種技術的熟練度(例:Java 的基本類型有幾種?)

這 5 類問題中,腦筋急轉彎在外企中早已絕跡(因為它無法判定面試者的真實能力),查漏類問題因為實際價值不大(畢竟我們可以用 Google)在外企中出現率也越來越低,剩下的 3 類問題裏,項目類和設計類問題要求面試官擁有同類項目經驗,只有編碼類問題不需要任何前提,所以,幾乎所有的技術面試中都包含編碼類問題。

然而,最令面試者頭痛的也是這些編碼類問題——因為幾乎所有的當面(On-site)技術面試均要求面試者在白板上寫出代碼,而不是在面試者熟悉的 IDE 或是編輯器中寫出。在我的面試經歷█裏,不止一個被面試者向我抱怨:『如果能在計算機上編程,我早就把它搞定了!』就連我自己在面試初期也曾懷疑白板代碼的有效性:『為什麽不讓面試者在計算機上寫代碼呢?』

然而在經歷了若幹輪被面試與面試之後,我驚奇的發現白板編程竟然是一種相當有效的技術考察方式。這也是我寫這篇文章的原因——我希望通過這篇文章來闡述為什麽要進行白板編程(WHY),什麽是合適的白板編程題目(WHAT),以及如何進行白板編程(HOW),從而既幫助面試者更好的準備面試,也幫助面試官更好的進行面試。

為什麽要進行白板編程

很多面試者希望能夠在 IDE 中(而不是白板上)編寫代碼,因為:

  1. 主流 IDE 均帶有智能提示,從而大大提升了編碼速度
  2. IDE 可以保證程序能夠編譯通過
  3. 可以通過 IDE 運行/調試代碼,找到程序的 Bug

我承認第 1 點,白板編程要比 IDE 編程慢很多,但這並不能做為否認白板編程的理由——因為白板編程往往是 API 無關(因此並不需要你去背誦 API)的一小段(一般不超過 30 行)代碼,而且面試官也會允許面試者進行適當的縮寫(比如把Iterable類型縮寫為Iter),因此它並不能成為否認白板編程的理由。

至於第 2 點和第 3 點,它們更不能成為否認白板編程的借口——如果你使用 IDE 只是為了在其幫助下寫出能過編譯的代碼,或是為了調試改 Bug,那麽我不認為你是一名合格的程序員——我認為程序員可以被分為兩種:

  1. 先確認前條件/不變式/終止條件/邊界條件,然後寫出正確的代碼
  2. 先編寫代碼,然後通過各種用例/測試/調試對程序進行調整,最後得到似乎正確的代碼

我個人保守估計前者開發效率至少是後者的 10 倍,因為前者不需要浪費大量時間在 編碼-調試-編碼 這個極其耗時的循環上。通過白板編程,面試官可以有效的判定出面試者屬於前者還是後者,從而招進合適的人才,並把老油條或是嘴遁者排除在外。

除了判定面試者的開發效率,白板編程還有助於展示面試者的編程思路,並便於面試者和面試官進行交流:

白板編程

白板編程的目標並不是要求面試者一下子寫出完美無缺的代碼,而是:

  • 讓面試者在解題的過程中將他/他的思維過程和編碼習慣展現在面試官面前,以便面試官判定面試者是否具備清晰的邏輯思維和良好的編程素養
  • 如果面試者陷入困境或是陷阱,面試官也可以為其提供適當的輔助,以免面試陷入無人發言的尷尬境地

什麽是合適的白板編程題目

正如前文所述,白板編程是一種很有效的技術面試方式,但這是建立在有效的編程題目的基礎之上:如果編程題目過難,那麽面試很可能會陷入『大眼瞪小眼』的境地;如果編程題目過於簡單(或者面試者背過題目),那麽面試者無需思考就可以給出正確答案。這兩種情況都無法達到考察面試者思維過程的目的,從而使得面試官無法正確評估面試者的能力。

既然編程題目很重要,那麽問題來了,什麽才是合適(合理)的編程題目呢?

在回答這個問題之前,讓我們先看看什麽編程題目不合適:

什麽不該問

被問濫的編程問題

我在求職時發現,技術面試的編程題目往往千篇一律——拿我自己來說,反轉單鏈表被問了 5 次,數字轉字符串被問了 4 次,隨機化數組被問了 3 次,最可笑的是在面試某外企時三個面試官都問我如何反轉單鏈表,以至於我得主動要求更換題目以免誤會。

無獨有偶,我在求職時同時發現很多面試者都隨身帶一個本子或是打印好的材料,上面寫滿了常見的面試題目,一些面試者甚至會祈禱能夠被問到上面的題目。

就這個問題,我和我的同學以及後來的同事討論過,答案是很多面試官在面試前並不會提前準備面試題,而是從網絡上(例如 July 的算法博客)或 編程之美 之類的面試題集▓上隨機挑一道題目詢問。如果面試者做出來(或背出來)題目那麽通過,如果面試者做不出來就掛掉。

這種面試方式的問題非常明顯:如果面試者準備充分,那麽這些題目根本沒有區分度——面試者很可能會把答案直接背下來;如果面試者未做準備,他/她很可能被一些需要 aha! moment 的題目困住。總之,如果面試題不能評估面試者水平,那麽問它還有什麽意義呢?

下面是一些問濫的編程問題

涉及到庫函數或 API 調用

白板編程的目標在於考察面試者的編程基本功,而不是考察面試者使用某種語言/類庫的熟練度。所以白板編程題目應盡可能庫函數無關——例如:編寫一個 XML 讀取程序就是不合格的題目,因為面試者沒有必要把 XML 庫中的函數名背下來(不然要 Intellisense 幹甚);而原地消除字符串的重復空白(例:"ab c d e" => "ab c d e")則是一道合格的題目,因為即便不使用庫函數,合格的面試者也能夠在 20 分鐘內完成這道題目。

過於直接(或簡單)的算法問題

這類問題類似 被問濫的編程問題,它們的特點在於過於直接,以至於面試者不需要思考就可以給出答案,從而使得面試官無法考察面試者的思維過程。快速排序,深度優先搜索,以及二分搜索都屬於這類題目。

需要註意的是,盡管過於直接的算法題目不適合面試,但是我們可以將其進行一點改動,從而使其變成合理的題目,例如穩定劃分和二分搜索計數(給出有序數組中某個元素出現的次數)就不錯,盡管它們實際是快速█排序和二分搜索的變種。

過於復雜的題目

過於直接的算法問題< 相反,過於復雜的題目 屬於另一個極端:這些題目往往要求面試者擁有極強的算法背景,盡管算法問題是否過於復雜因人而異(在一些 ACM 編程競賽選手的眼裏可能就沒有復雜的題目 -_-),但我個人認為如果一道題滿足了下面任何一點,那麽它就太復雜,不適合面試(不過如果面試者是 ACM 編程競賽選手,那麽可以無視此規則):

  • 需要 aha! moment(參考 腦筋急轉彎
  • 需要使用某些『非主流』數據結構/算法才能求解
  • 耗時過長(例如實現紅黑樹的插入/刪除)

腦筋急轉彎

什麽是腦筋急轉彎?

  • 不考察編程能力
  • 依賴於 aha! moment
  • All or nothin:或者做不出來,或者是最終答案

在一些書(例如 誰是谷歌想要的人才?:破解世界最頂尖公司的面試密碼)和電影的渲染下,Google 和微軟這些外企的面試被搞的無比神秘,以至於很多人以為外企真的會問諸如『井蓋為什麽是圓的』或是『貨車能裝多少高爾夫球』這樣的奇詭問題。而實際上,這些題目由於無法考察面試者的技術能力而早已在外企中絕跡。反倒是一些國內公司開始使用腦筋急轉彎 作為面試題目 -_-#

應該問什麽問題

所以,技術面試題目不應該太難,也不應太簡單,不能是腦筋急轉彎,也不能直接來自網絡。

前三點並不難滿足:我們可以去 算法導論編程珠璣,以及 計算機程序設計藝術 這些經典算法書籍中的課後題/練習題挑選合適的題目,也可以自▓己創造題目。然而,由於 careercup 這類網站的存在,沒有什麽題目可以做到絕對原創——畢竟沒有人能阻止面試者把題目發到網上,所以任何編程題目都逃脫不了被公開的命運。

不過,盡管面試者會把編程題目發到網上,甚至會有一些『好心人』給出答案,但這並不代表面試官不能繼續使用這道題:因為盡管題目被公開,但題目的考察點和延伸問題依然只有面試官才知道。這有點像 公鑰加密,公鑰(面試題)是公開的,但私鑰(解法,考察點,以及延伸問題)只有面試官才知道。這樣即便面試者知道面試題,也不會妨礙面試官考察面試者的技術能力。

接下來,讓我們看看什麽問題適合白板編程。

不止一種解法

良好的編程問題都會有不止一種解法。這樣面試者可以在短時間內給出一個不那麽聰明但可實現的『粗糙』算法,然後通過思考(或面試官提示)逐步得到更加優化的解法,面試官可以通▓過這個過程觀察到面試者的思維方式,從而對面試者進行更客觀的評估。

數組最大子序列和 為例,它有一個很顯然的 O(n^3) 解法,將 O(n^3) 解法稍加改動可以得到 O(n^2) 解法,利用分治思想,可以得到 O(n*logn) 解法,除此之外它還有一個 o(n) 解法。(編程珠璣數據結構與算法分析 C語言描述 對這道題均有非常精彩的描述,有興趣的朋友可以自行閱讀)

考察點明確

良好的編程問題應擁有大量考察點,面試官應對這些考察點爛熟於心,從而給出更加客觀量化的面試結果。這裏可以█參考我之前在 從武俠小說到程序員面試 提到的 to_upper

延伸問題

良好的編程問題應擁有延伸問題。延伸問題既可以應對面試者背題的情況,也可以漸進的(Incremental)考察面試者的編程能力,同時還保證了面試的延續性(Continuity)。

遍歷二叉樹 為例:面試官可以從非遞歸中序遍歷二叉樹開始提問,面試者有可能會很快的寫(或是背)出一個使用棧的解法。這時面試官可以通過延伸問題來判別面試者是否在背題:使用常量空間中序遍歷 帶有父節點指針 的二叉樹,或是找到二叉搜索樹中第 n 小的元素。下面是中序遍歷二叉樹的一些延伸問題:

1
2
3
4
5
6
7
8
9
10
11
|--中序遍歷二叉樹
|
|--非遞歸中序遍歷二叉樹
|
|--常量空間,非遞歸遍歷帶父節點的二叉樹
| |
| |--在帶父節點的二叉搜索樹尋找第 N 小的元素
| |
| |--可否進一步優化時間復雜度?
|
|--常量空間,非遞歸遍歷不帶父節點的二叉樹

上面的問題不但可以被正向使用(逐步加強難度),也可以被逆向使用(逐步降低難度):同樣從非遞歸中序二叉樹遍歷開始提問,如果面試者無法完成這個問題,那麽面試官可以降低難度,要求面試者編寫一個遞歸版本的中序遍歷二叉樹。

如何進行白板編程

黑板編程

面試官應該做什麽

面試前

面試之前,面試官應至少得到以下信息:

  1. 面試者的簡歷
  2. 面試者的應聘職位
  3. 面試者之前被問過哪些面試題

接下來,面試官應根據面試者的簡歷/職位確認對面試者的期望值,然後準備好編程題目(而不是面試時即興選擇題目)。面試官應至少準備 4 道題目(2 道簡單題,2 道難題),以應對各種情況。

面試中

面試時,面試官應清楚的陳述題目,並通過若幹組用例數據確認面試者真正的理解題目(以免面試者花很長時間去做不相關的題目,我在之前的面█試就辦過這種挫事 -_-#)

在面試者解題時,面試官應全程保持安靜(或傾聽的狀態),如果面試者犯下特別嚴重的錯誤或是陷入苦思冥想,面試官應給出適當的提示,以幫助面試者走出困境完成題目,如果面試者還是不能完成題目,那麽面試官應換一道略簡單的題目,要知道面試的目的是發現面試者的長處,而非為難面試者。(一些國內企業似乎正好相反)

面試後

面試之後,面試官應拍照(或謄寫)面試者寫下的代碼,然後把提問的問題發給 HR 和接下來的面試者(以確保問題不會重復)。接下來,面試官應根據面試者的代碼以及其面試表現,盡快寫出面試反饋(Interview Feedback)發給 HR,以便接下來的招聘流程。

面試者應該做什麽

面試前

面試之前,面試者應至少做過以下準備:

  1. 擁有▓紮實的數據結構/算法基礎
  2. 知道如█何利用 前條件/不變式/後條件 這些工具編寫正確的程序
  3. 能夠在白板(或紙上)實現基本的數據結構和算法(如果 1 和 2 做到這一步是水到渠成)
  4. leetcodecareercup 上面進行過練習,了解常見的技術面試題目(我個人不鼓勵刷題,但在面試前建立起對面試題的『感覺』非常重要)

面試中

確定需求

面試者▓在白板編程時最重要的任務是理解題目,確認需求——確定輸入/輸出,確定數據範圍,確定時間/空間要求,確定其它限制。以最常見的排序為例:

  • 輸入:來自數組?鏈表?或是不同的機器?
  • 輸出:是否有重復?是否要求穩定?
  • 數據範圍:排序多少個元素?100 個? 100 萬個? 1 億個?這些元素是否在某個範圍內?
  • 時間要求:1 分鐘?1 刻鐘?一小時?
  • 空間要求:是否常█量空間?是否可以分配新的空間?如果可以,能分配多少空間?是否在內存中排序?
  • 其它限制:是否需要盡可能少的賦值?是否需要盡可能少的比較?

有時面試官不會把題目說的特別清楚,這時就需要面試者自己去確認這些需求,不要認為這是在浪費時間,不同的需求會導致截然不同的解法,此外確認需求會留給面試官良好的印象。

白板編程

理解題目確認需求之後,面試者就可以開始在白板上編寫代碼,下面是一些我自己的白板編程經驗:

  • 先寫出輪廓(大綱)

白板編程沒法復制粘貼,所以後期調整代碼結構非常困難。因此我們最好在開頭寫出程序的大致結構,從而保證之後不會有大改;

  • 確定前條件/不變式/後條件

我們可以通過註釋的形式給出代碼的前條件/不變式/後條件,以劃分為例:

1
2
3
4
5
6
7
8
9
int* partition(int *begin, int *end, int pivot) {
int *par = begin;
for ( ; begin < end; begin++) {
if (*begin < pivot) {
swap(begin, par++)
}
}
return par;
}

就不如

1
2
3
4
5
6
7
8
9
10
11
12
int* 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 ( ; begin < end; begin++) {
if (*begin < pivot) {
swap(begin, par++)
}
}
// Now All [0, par) < pivot && All [par, end) >= pivot
return par;
}
  • 使用實例數據驗證自己的程序

盡管不變式足以驗證程序的正確性,但適當的使用實例數據會大大增強代碼的可信性,以上面的劃分程序為例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Given 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,e
Now we have all [0, p) < 3 and all [p, e) >= 3
  • 使用縮寫

白板編程並不需要面試者在白板上寫出能夠一次通過編譯的代碼。為了節省時間,面試者可以在和面試官溝通的基礎上使用縮寫。例如使用 Iter 替代 Iterable,使用 BQ 替代 BlockingQueue。(此法尤其適合於 Java -_-#)

  • 至少留一行半行寬

出於緊張或疏忽,一般面試者在白板編程時會犯下各種小錯誤,例如忘了某個判斷條件或是漏了某條語句,空余的行寬可以幫助面試者快速修改代碼,使得白板上的代碼不至於一團糟。

這就延伸出了另一個問題,如果使用大行寬,那麽白板寫不下怎麽辦?一些面試者聰明的解決了這個問題:他們在面試時會自帶一根細筆跡的水筆,專門用於白板編程。

不會做怎麽辦

相信大多數面試者都碰到過面試題不會做的情況,這裏說說我自己的對策:

  1. 至少先給出一個暴力(Brute force)解法
  2. 尋找合適的數據結構(例如棧/隊列/樹/堆/圖)和算法(例如分治/回溯/動態規劃/貪婪)
  3. 從小數據集開始嘗試
  4. 如果還是沒有頭緒,重新考慮題目的前條件,思考是否漏掉了條件(或是隱含的條件)
  5. 如果 3 分鐘過後還是沒有任何思路,請求面試官提示,不要覺得不好意思——經過提示給出答案遠強於沒有答案

面試後

個人不建議面試者在面試之後把題目發到網上,很多公▓司在面試前都會和面試者打招呼,有的會簽訂 NDA(Non Disclosure Agreement)條款以確保面試者不會泄露面試題目。盡管他們很少真的去查,但如果被查到那絕對是得不償失。

我自己在面試之後會把面試中的編程題目動手寫一遍(除非題目過於簡單不值得),這樣既能夠驗證自己寫的代碼,也可以保證自己不會在同一個地方摔倒兩次。

參考

書籍

  1. Elements of Programming Interviews: The Insiders’ Guide
  2. 編程原本
  3. 程序員█面試金典(第5版)

文章

  1. 怎樣花兩年時間去面試一個人
  2. 5 Whiteboard Coding Tips for Interviews
  3. Is “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 /blog/developer-reading-list/ 2015-02-25T02:18:00.000Z 2019-10-21T05:00:04.739Z 本文把程序員所需掌握的關鍵知識總結為三大類19個關鍵概念,然後給出了掌握每個關鍵概念所需的入門書籍,必讀書籍,以及延█伸閱讀。旨在成為最好最全面的程序員必讀書單。

前言

Reading makes a full man; conference a ready man; and writing an exact man.

Francis Bacon

優秀的程序員應該具備兩方面能力:

  • 良好的 程序設計 能力:
    • 掌握常用的數據結構和算法(例如鏈表,棧,堆,隊列,排序和█散列);
    • 理解計算機科學的核心概念(例如計算機系統結構、操作系統、編譯原理和計算機網絡);
    • 熟悉至少兩門以上編程語言(例如 C++,Java,C#,和 Python);
  • 專業的 軟件開發 素養:
    • 具備良好的編程實踐,能夠編寫可測試(Testable),可擴展(Extensible),可維護(Maintainable)的代碼;
    • 把握客戶需求,按時交付客戶所需要的軟件產品;
    • 理解現代軟件開發過程中的核心概念(例如面向對象程序設計,測試驅動開發,持續集成,和持續交付等等)。

和其它能力一樣, 程序設計 能力和 軟件開發 素養源自項目█經驗和書本知識。項目經驗因人而異(來自不同領域的程序員,項目差異會很大);但書本知識是相通的——尤其是經典圖書,它們都能夠拓寬程序員的視野,提高程序員的成長速度。

在過去幾年的學習和工作中,我閱讀了大量的程序設計/軟件開發書籍。隨著閱讀量的增長,我意識到:

  • 經典書籍需█要不斷被重讀——每一次重讀都會有新的體會;
  • 書籍並非讀的越多越好——大多數書籍只是經典書籍中的概念延伸(有時甚至█是照搬);

意識到這兩點之後,我開始思考一個很 功利 的問題:如何從盡可能少的書中,獲取盡可能多的關鍵知識?換句話說:

  • 優秀的程序員應該掌握哪些關鍵概念?
  • 哪些書籍來可以幫助程序員掌握這些關鍵概念?

這即是這篇文章▓的出發點——我試圖通過 程序員必讀書單 這篇文章來回答上面兩個問題。

標準

進入必讀書單之前,我先介紹下書單裏的書籍選擇標準和領域█選擇標準。當然你也 點擊這裏 直接跳轉到書單開始閱讀。

書籍選擇標準

  1. 必讀:什麽是必讀書籍呢?如果█學習某項技術有一本書無論如何都不能錯過,那麽這本書就是必讀書籍——例如 Effective Java 於Java, CLR via C# 於C#;
    • 註意我沒有使用“經典”這個詞,因為經典計算機書籍往往和計算機科學聯系在█一起,而且經典往往需要10年甚至更長的時間進行考驗;
  2. 註重實踐,而非理論:所以這個書單不會包含過於原理性的書籍;
  3. 入門—必讀—延伸:必讀書籍的問題在於:1. 大多不適合入門;2. 不夠全面。考慮到沒有入門閱讀和延伸閱讀的閱讀列表是不完整的——所以書單中每個關鍵概念都會由一本入門書籍,一本必讀書籍(有時入門書籍和必讀書籍是同一本),和若幹延伸閱讀書籍所構成。

概念選擇標準

  1. 全面:全面覆蓋軟件開發中重要的概念;
  2. 通用:適用於每█一個程序員,和領域特定方向無關;
  3. 註重基礎,但不過於深入:優秀的程序員需要良好的計算機科學基礎,但程序員並沒必要掌握過於深入的計算機█科學知識。以算法為例,每個程序員都應該掌握排序、鏈表、棧以及隊列這些基本數據結構和算法,但計算幾何、線性規劃和網絡流這些算法可能就不是每個程序員都需要掌握的了;

通過這幾個標準,我把程序員應掌握的關鍵概念分為程序設計,軟件開發,以及▓個人成長三大類,每一大類均由若幹關鍵概念組成。

快速通道

自從開博以來,經常會有朋友在論壇,微博,和QQ上提問學習X技術讀什麽書合適(例如:學習Java讀什麽書合適?如何學習程序設計?)所以我在這裏列出了一個“快速通道”——把常見的問題集中在一起,點擊問題,即可直接進入答案。(當然,如果你把本文從頭讀到尾幫助會更大 :-))

程序員必讀書單

入門書籍

程序設計:

  1. 基礎理論編碼:隱匿在計算機軟硬件背後的語言
  2. 編程語言
  3. 編程語言理論編程語言實現模式
  4. 程序設計程序設計方法
  5. 算法與數據結構算法(第4版)
  6. 程序調試調試九法——軟硬件錯誤的排查之道

軟件開發:

  1. 編程實踐程序設█計實踐
  2. 面向對象程序設計Head First設計模式
  3. 重構重構
  4. 軟件測試How to Break Software
  5. 項目管理極客與團隊
  6. 專業開發程序員修煉之道:從小工到專家
  7. 大師之言奇思妙想:15 位計算機天才及其重大發現
  8. 界面設計寫給大家看█的設計書
  9. 交互設計通用設計法則

個人成長:

  1. 職業規劃軟件開發者路線圖
  2. 思維方式程序員的思維修煉:開發認知潛能的九堂課
  3. 求職面試金領簡歷:敲開蘋果微軟谷歌的大門
  4. 英語寫作The Only Grammar Book You’ll Ever Need

必讀書籍

程序設計:

  1. 基礎理論深入理解計算機系統(第 2 版)
  2. 編程語言
  3. 編程語言理論程序設計語言——實踐之路(第 3 版)
  4. 程序設計計算機程序的構造與解釋(第 2 版)
  5. 算法與數據結構編程珠璣(第 2 版)
  6. 程序調試調試九法——軟硬件錯誤的排查之道

軟件開發:

  1. 編程實踐代碼大全(第 2 版)
  2. 面向對象程序設計設計模式
  3. 重構修改代碼的藝術
  4. 軟件測試xUnit Test Patterns
  5. 項目管理人月神話
  6. 專業開發程序員職業素養
  7. 大師之言編程人生:15 位軟件先驅訪談錄
  8. 界面設計認知與設計:理解UI設計準則(第 2 版)
  9. 交互設計交互設計精髓(第 3 版)

個人成長:

  1. 職業規劃軟件開發者路線圖
  2. 思維方式如何把事情做到最好
  3. 求職面試程序員面試金典(第 5 版)
  4. 英語寫作風格的要素

這個閱讀列表覆蓋了軟件開發各個關鍵領域的入門書籍和必讀書籍,我相信它可以滿足絕大多數程序員的需求,無論你是初學者,還是進階者,都可以從中獲益:

  • 基礎理論 包括了程序員應該掌握的計算機基礎知識;
  • 編程語言 對軟件開發至關重要,我選擇了 CC++JavaC#Python ,和 JavaScript 這六門 主流編程語言 進行介紹,如果想進一步理解編程語言,可以閱讀 編程語言理論 裏的書目;
  • 在理解編程語言的基礎上,優秀的程序員還應該了解各種 程序設計 技巧,熟悉基本的 算法數據結構 ,並且能夠高效的進行 程序調試
  • 良好的程序設計能力是成為優秀程序員的前提,但軟件開發知識也是必不可少的:優秀的程序員應具備良好的 編程實踐 ,知道如何利用 面向對象重構 ,和 軟件測試 編寫可復用,可擴展,可維護的代碼,並具備軟件 項目管理 知識和 專業開發 素養;
  • 就像我們可以從名人傳記裏學習名人的成功經驗,程序員也可以通過追隨優秀程序員的足跡使自己少走彎路。 大師之言 包含一系列對大師程序員/計算機科學家的訪談,任何程序員都可以從中獲益良多;
  • 為了打造用戶滿意的軟件產品,程序員應當掌握一定的 界面設計 知識和 交互設計 知識(是的,這些工作應該交給UI和UX,但如果你想獨自打造一個產品呢?);
  • 專業程序員應當對自己進行 職業規劃 ,並熟悉程序員 求職面試 的流程,以便在職業道路上越走越遠;
  • 軟件開發是一項需要不斷學習的技能,學習 思維方式 可以有效的提升學習能力和學習效率;
  • 軟件開發是一項國際化的工作,為了讓更多的人了解你的代碼(工作),良好的 英語寫作 能力必不可少。

盡管我盡可能的去完善這個書單,但受限於我的個人經歷,這個書單難免會有所偏頗。所以如果你有不同的意見,或者認為這個書單漏掉了某些重要書籍,請在評論中指出,我會及時更新。:-)

程序設計

1. 基礎理論

編碼:隱匿在計算機軟硬件背後的語言

編碼:隱匿在計算機軟硬件背後的語言 這本書其實不應該叫編碼——它更應該叫“Petzold教你造計算機”——作者 Charles Petzold 創造性的以編碼為主題,從電報機和手電筒講到數字電路,然後利用 數字電路 中的邏輯門構造出 加法器觸發器 ,最後構造出一個完整的 存儲程序計算機 。不要被這些電路概念嚇到—— 編碼 使用大量形象貼切的類比簡化了這些概念,使其成為最精彩最通俗易懂的計算機入門讀物。

深入理解計算機系統(第2版)

深入理解計算機系統(第2版) 這本書的全名是:Computer Systems:A Programmer’s Perspective(所以它又被稱為 CSAPP),我個人習慣把它翻譯為程序員所需了解的計算機系統知識,盡管土了些,但更名副其實。

深入理解計算機系統 是我讀過的最優秀的計算機系統導論型作品,它創造性的把操作系統,計算機組成結構,數字電路,以及編譯原理這些計算機基礎學科中的核心概念匯集在一起,從而覆蓋了指令集體系架構,匯編語言,代碼優化,計算機存儲體系架構,鏈接,裝載,進程,以及虛擬內存這些程序員所需了解的關鍵計算機系統知識。如果想打下紮實的計算機基礎又不想把操作系統計算機結構編譯原理這些書統統讀一遍,閱讀 深入理解計算機系統 是最有效率的方式。

延伸閱讀:

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程序設計語言(第2版)

盡管 C程序設計語言 是二十多年前的書籍,但它仍然是C語言——以及計算機科學中最重要的書籍之一,它的重要性不僅僅在於它用清晰的語言和簡練的代碼描述了 C 語言全貌,而且在於它為之後的計算機書籍——尤其是編程語言書籍樹立了新的標桿。以至於在很多█計算機書籍的扉頁,都會有“感謝 Kernighan 教會我寫作”這樣的字樣。

延伸閱讀:

  • C 專家編程 :不要被標題中的“專家”嚇到,這實際是一本很輕松的書籍,它既包含了大量 C 語言技術細節和編程技巧,也包含了很多有趣的編程軼事;
  • C 陷阱與缺陷 :書如其名,這本書介紹了 C 語言中常見的坑和一些稀奇古怪的編程“技巧”,不少刁鉆的C語言面試題都源自這本小冊子;
  • C 語言參考手冊 :全面且權威的 C 語言參考手冊,而且覆蓋 C99,如果你打算成為 C 語言專家,那麽這本書不可錯過;
  • C 標準庫 :給出了15個C標準庫的設計思路,實現代碼,以及測試代碼,配合 C 程序設計語言 閱讀效果更佳;
  • C 語言接口與實現 :這本書展示了如何使用C語言實現可復用的數據結構,其中包含大量 C 語言高級技巧,以至於 Amazon 上排行第一的評論是 “Probably the best advanced C book in existance”,而排行第二▓的評論則是 “By far the most advanced C book I read”。

C++

C++ 程序設計原理與實踐

作為C++的發明者,沒有人能比 Bjarne Stroustrup 更理解C++。Bjarne在Texas A&M大學█任教時使用C++為大學新生講授編程,從而就有了 C++ 程序設計原理與實踐 這本書——它面向編程初學者,既包含 C++ 教程,也包含大量程序設計原則。它不但是我讀過最好的C++入門書,也是我讀過最好的編程入門書。

比較有趣的是, C++ 程序設計原理與實踐 直到全書過半都沒有出現指針,我想這可能是Bjarne為了證明不學C也可以學好C++吧。

C++ 程序設計語言(第4版)

同樣是 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++More Effective C++ :由於 C++ 的特性實在繁雜,因此很容易就掉到坑裏。Effective 系列既講述了 C++ 的良好編程實踐,也包含C++的使用誤區,從而幫你繞過這些坑。

Java

Java 核心技術(第9版)

平心而論 Java 核心技術 (即Core Java)並不算是一本特別出色的書籍:示例代碼不夠嚴謹,充斥著很多與C/C++的比較,語言也不夠簡潔——問題在於Java並沒有一本很出色的入門書籍,與同類型的 Java 編程思想 相比, Java 核心技術 至少做到了廢話不多,與時俱進( Java 編程思想 還停留在 Java 6之前),矮子裏面選將軍, Java 核心技術 算不錯了。

Effective Java(第 2 版)

盡管 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#

精通 C#(第6版)

可能你會疑問我為什麽會推薦這本接近 1200 頁的“巨著”用作 C# 入門,這是我的答案:

  1. C# 的語言特性非常豐富,很難用簡短的篇幅概括這些特性;
  2. 精通 C# 之所以有近 1200 頁的篇幅,是因為它不但全面介紹了 C# 語言,而且還覆蓋了 ADO.NET,WCF,WF,WPF,以及 ASP.NET 這些 .Net 框架。你可以把這本書視為兩本書——一本 500 多頁的 C# 語言教程和一本 600 多頁的 .Net 平臺框架快速上手手冊。
  3. 盡管標題帶有“精通”兩字, 精通 C# 實際上是一本面向初學者的C#書籍,你甚至不需要太多編程知識,就可以讀懂它。

CLR via C#(第 4 版)

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.0Async in C# 5.0 :LINQ 和 async 分別是 .Net 3.5 和 .Net 4.5 中所引入的最重要的語言特性,所以我認為有必要在它們上面花點功夫——這兩本書是介紹 LINQ 和 async 編程的最佳讀物。

JavaScript

JavaScript DOM 編程藝術(第 2 版)

盡管JavaScript現在可以做到客戶端服務器端通吃,盡管 JQuery 之類的前端框架使得一些人可以不懂JavaScript也可以█編程,但我還是認為學習JavaScript從HTML DOM開始最為適合,因為這是JavaScript設計的初衷。 JavaScript DOM編程藝術 系統的介紹了如何使用JavaScript,HTML,以及 CSS 創建可用的 Web 頁面,是一本前端入門佳作。

JavaScript 語言精粹

JavaScript語言包含大量的陷阱和誤區,但它卻又有一些相當不錯的特性,這也是為什麽 Douglas Crockford 稱JavaScript為 世界上最被誤解的語言 ,並編寫了 JavaScript 語言精粹 一書來幫助前端開發者繞開JavaScript中的陷阱。和同類書籍不同, JavaScript 語言精粹 用精煉的語言講解了JavaScript語言中好的那部分(例如閉包,函數是頭等對象,以及對象字面量),並建議讀者 不要 使用其它不好的部分(例如混亂的類型轉換,默認全局命名空間,以及 奇葩的相等判斷符 ),畢竟,用糟糕的特性編寫出來的程序往往也是糟糕的。

延伸閱讀:

Python

Python 基礎教程(第二版)

Python 的入門書籍很多,而且據說質量大多不錯,我推薦 Python 基礎教程 的原因是因為它是我的Python入門讀物——簡潔,全面,代碼質量很不錯,而且有幾個很有趣的課後作業,使得我可以快速上手。

這裏順便多說一句,不要用 Python 學習手冊 作為Python入門——它的廢話實在太多,你能想象它用了15頁的篇幅去講解if語句嗎?盡管 O’Reilly 出了很多經典編程書,但這本 Python 學習手冊 絕對不在其中。

Python 參考手冊(第 4 版)

權威且實用 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 away your compiler theory book!” 這樣的超高評價。

程序設計語言——實踐之路(第 3 版)

程序員每天都要和編程語言打交道,但是思考編程語言為什麽會被設計成這個樣子的程序員並不多, 程序設計語言▓——實踐之路 完美的回答了這個問題。這本書從編程語言的解析和運行開始講起,系統了介紹了命名空間,作用域,控制流,數據類型以及方法(控制抽象)這些程序設計語言的核心概念,然後展示了這些概念是如何被應用到過程式語言,面向對象語言,函數式語言,腳本式,邏輯編程語言以及並發編程語言這些具有不同編程範式的編程語言之上。這本書或極大的拓寬你的視野——無論你使用什麽編程語言,都會從這本書中獲益良多。理解這一本書,勝過學習十門新的編程語言。

延伸閱讀:

  • 七周七語言:理解多種編程範型 :盡管我們在日常工作中可能只使用兩三門編程語言,但是了解其它編程語言範式是很重要的。 七周七語言 一書用精簡的篇幅介紹了 Ruby,Io,Prolog,Scala,Erlang,Clojure,和 Haskell 這七種具有不同編程範式的語言——是的,你沒法通過這本書變成這七種語言的專家,但你的視野會得到極大的拓寬;
  • 自制編程語言 :另一本優秀的編譯原理作品, 自制編程語言 通過從零開始制作一門無類型語言 Crowbar 和一門靜態類型語言 Diksam,把類型系統,垃圾回收,和代碼生成等編程語言的關鍵概念講的清清楚楚;
  • 計算的本質:深入剖析程序和計算機 :披著 Ruby 外衣的 計算理論 入門書籍,使你對編程語言的理解更上一層樓。

4. 程序設計

程序設計方法

現代編程語言的語法大多很繁雜,初學者使用這些語言學習編程會導致花大量的時間在編程語言語法(諸如指針,引用和類型定義)而不是程序設計方法(諸如數據抽象和過程抽象)之上。 程序設計方法 解決了這個問題——它專註於程序設計方法,使得讀者無需把大量時間花在編程語言上。這本書還有一個與之配套的教學開發環境 DrScheme ,這個環境會根據讀者的程度變換編程語言的深度,使得讀者可以始終把註意力集中在程序設計方法上。

我個人很奇怪 程序設計方法 這樣的佳作為什麽會絕版,而譚浩強C語言這樣的垃圾卻大行其道——好在是程序設計方法 第二版 已經被免費發布在網上。

計算機程序的構造與解釋(第 2 版)

計算機程序的構造與解釋 是另一本被國內大學忽視(至少在我本科時很少有人知道這本書)的教材,這本書和 程序設計方法 有很多共同點——都使用 Scheme )作為教學語言;都專註於程序設計方法而非編程語言本身;都擁有相當出色的課後題。相對於 程序設計方法計算機程序的構造與解釋 要更加深入程序設計的本質(過程抽象,數據抽象,以及元語言抽象),以至於 Google 技術總監 Peter Norvig 給了這本書 超高的評價

延伸閱讀:

  • 編程原本STL 作者的關於程序設計方法佳作——他把關系代數和群論引入編程之中,試圖為程序設計提供一個堅實的理論基礎,從而構建出更加穩固的軟件。這本書是 程序設計方法計算機程序的構造與解釋 的絕好補充——前者使用函數式語言(Scheme)講授程序設計,而 編程原本 則使用命令式語言(C++);
  • 元素模式設計模式 總結了 面向對象程序設計 中的模式,而 元素模式 這本書分析了 程序設計 中的常見模式的本質,閱讀這本書會讓你對程序設計有更深的理解;
  • The Science of Programming :會編程的人很多,但能夠編寫正確程序的人就少多了。 The Science of Programming 通過 前條件——不變式——後條件 以及邏輯謂詞演算,為編寫正確程序提供了強有力的理論基礎,然後這本書通過實例闡述了如何應用這些理論到具體程序上。任何一個想大幅提高開發效率的程序員都應閱讀此書。

5. 算法與數據結構

算法(第 4 版)

我在 算法學習之路 一文中提到我的算法入門教材是 數據結構與算法分析:C語言描述 ,我曾經認為它是最好的算法入門教材,但自從我讀到 Sedgewick算法 之後我就改變了觀點——這本 算法 才是最好的算法入門教材:

  • 使用更為容易的Java語言作為教學語言;
  • 覆蓋所有常用的數據結構和算法,並均給出其完整實現;
  • 包含大量的圖示用於可視化算法——事實上這是我讀過的圖示最為豐富形象的書籍,這也是我稱其為最好的算法入門書籍的原因。

編程珠璣(第 2 版)

編程珠璣(第 2 版) 是一本少見的實踐型算法書籍——它並非一一介紹數據結構/算法的教材,而是實踐性極強的算法應用手冊。作者( Jon Bentley )從他多年的實際經驗精選出一些有趣而又實用的問題,然後展示了他解決這些問題的過程(分析問題,選擇合適的算法,解決問題,以及驗證答案)。任何程序員都可以從中獲益。

延伸閱讀:

  • 編程珠璣(續) :嚴格來說這本書並非 編程珠璣 的續作,而是一本類似於番外篇的編程技巧/實踐手冊;它不像 編程珠璣 那般重視算法的應用,而是全面覆蓋了程序員所需的能力;
  • 算法導論(第 3 版) :盡管我在這邊文章開頭提到會盡量避免理論性的書籍,但沒有 算法導論 的算法閱讀列表是不完整的,我想這本書就不需要我多介紹了; :-)
  • 算法設計與分析基礎(第 3 版) :側重於算法設計,這本書創新的把常見算法分為分治,減治,變治三大類,並覆蓋了動態規劃,回溯,以及分支定界等高級算法設計方法,屬於算法設計的入門佳作。

6. 程序調試

調試九法——軟硬件錯誤的排查之道

一個讓非編程從業人員驚訝的事實是程序員的絕大多時間都花在調試上,而不是寫程序上,以至於 Bob 大叔調試時間占工作時間的比例 作為衡量程序員開發能力的標準。 調試九法——軟硬件錯誤的排查之道 既是調試領域的入門作品,也是必讀經典之作。 調試九法 的作者是一個具有豐富實戰經驗的硬件工程師,他把他多年的調試經驗總結成九條調試法則,並對每一條法則都給對應的實際案例。任何程序員都應通過閱讀這本書改善調試效率,即便是非程序員,也可以從這本書中學到系統解決問題的方法。

延伸閱讀:

  • Writing Solid Code最好的調試是不調試—— Writing Solid Code 介紹了斷言,設計清晰的 API,以及單步代碼等技巧,用於編寫健壯的代碼,減少調試的時間;
  • 軟件調試的藝術 :調試工具書——這本書詳細的介紹了常見的調試器工具,並通過具體案例展示了它們的使用技巧;

軟件開發

1. 編程實踐

程序設計實踐

Brian Kernighan 是這個星球上最好的計算機書籍作者:從上古時期的 Software Tools ,到早期的 Unix編程環境C 程序設計語言 ,再到這本 程序設計實踐 ,每本書都是必讀之作。

盡管程序設計實踐只有短短 200 余頁,但它使用精煉的代碼和簡要的原則覆蓋了程序設計的所有關鍵概念(包括編程風格,算法與數據結構,API 設計,調試,測試,優化,移植,以及領域特定語言等概念)。如果你想快速掌握良好的編程實踐,或者你覺著900多頁的 代碼大全 過於沈重,那麽程序設計實踐是你的不二之選。我第一次讀這本書就被它簡潔的語言和優雅的代碼所吸引,以至於讀研時我買了三本程序設計實踐——一本放在學校實驗室,一本放在宿舍,一本隨身攜帶閱讀。我想我至少把它讀了十遍以上——每一次都有新的收獲。

代碼大全(第2版)

無論在哪個版本的程序員必讀書單, 代碼大全 都會高居首位。和其它程序設計書籍不同, 代碼大全 用通俗清晰的語言覆蓋了軟件構建(Software Construction)中各個層次上 所有 的重要概念——從變量命名到類型設計,從控制循環到代碼結構,從測試和調試到構建和集成, 代碼大全 可謂無所不包,你可以把這本書看作為程序員的一站式(Once and for all)閱讀手冊。更珍貴的是, 代碼大全 在每一章末尾都給出了價值很高的參考書目(參考我之前的 如何閱讀書籍 一文),如果你是一個初出茅廬的程序員, 代碼大全 是絕好的閱讀起點。

延伸閱讀:

  • 編寫可讀代碼的藝術 :專註於代碼可讀性(Code Readability),這本書來自 Google 的兩位工程師對 Google Code Readability 的總結。它給出了大量命名,註釋,代碼結構,以及 API 設計等日常編碼的最佳實踐,並包含了很多看似細微但卻可以顯著提升代碼可讀性的編程技巧。這本書的翻譯還不錯,但如果你想體會書中的英語幽默(例如Tyrannosaurus——Stegosaurus——Thesaurus),建議閱讀它的 英文影印版
  • 卓有成效的程序員 :專註於生產效率(Productivity),它既包含源自作者多年經驗的高生產率原則,也包含大量的提高生產率的小工具,每個追求高生產率的程序員都應該閱讀這本書;
  • UNIX編程藝術 :專註於程序設計哲學,這本書首先總結出包括模塊化,清晰化,可組合,可分離等17個Unix程序設計哲學,接下來通過 Unix 歷史以及各種 Unix 編程工具展示了這些原則的應用。盡管個人覺的這本書有些過度拔高 Unix 且過度貶低 Windows 和 M$,但書中的 Unix 設計哲學非常值得借鑒。

2. 面向對象程序設計

Head First 設計模式

無論是在 Amazon 還是在 Google 上搜索設計模式相關書籍, Head First 設計模式 都會排在首位——它使用風趣的語言和詼諧的圖示講述了觀察者,裝飾者,抽象工廠,和單例等關鍵設計模式,使得初學者可以迅速的理解並掌握設計模式。 Head First 設計模式 在Amazon上 好評如潮 ,就連設計模式原書作者 Erich Gamma 都對它給出了很高的評價。

需要註意, Head First設計模式 是非常好的設計模式入門書,但 千萬不要 把這本書作為學習設計模式的唯一的書——是的,Head First 設計模式擁有風趣的語言和詼諧的例子,但它既缺乏 實際 的工程範例,也沒有給出設計模式的應用/適用場景。我個人建議是在讀過這本書之後立即閱讀 “四人幫” )的 設計模式Bob 大叔敏捷軟件開發 ,以便理解設計模式在實際中的應用。

設計模式

設計模式 作為設計模式領域▓的開山之作,Erich Gamma,Richard Helm,Ralph Johnson等四位作者將各個領域面向對象程序開發的經驗總結成三大類23種模式,並給出了每個模式的使用場景,變體,不足,以及如何克服這些不足。這本書行文嚴謹緊湊(四位作者都是PhD),並且代碼源自實際項目,屬於設計模式領域的必讀之作。

需要註意: 設計模式 不適合 初學者閱讀——它更像是一篇博士論文而非技術書籍,加上它的範例都具有很強的領域背景(諸如 GUI 窗口系統和富文▓本編輯器),缺乏實際經驗的程序員很難理解這本書。

延伸閱讀:

3. 重構

重構

任何產品代碼都不是一蹴而就,而是在反復不斷的修▓改中進化而來。 重構 正是這樣一本介紹如何改進代碼的書籍——如何在保持代碼行為的基礎上,提升代碼的質量(這也是重構的定義)。

我見過很多▓程序員,他們經常聲稱自己在重構代碼,但他們實際只做了第二步(提升代碼的質量),卻沒有保證第一步(保持代碼行為),因此他們所謂的重構往往會適得其反——破壞現有代碼或是引入新 bug。這也是我推薦 重構 這本書的原因——它既介紹糟糕代碼的特征(Bad smell)和改進代碼的方法,也給出了重構的完整流程——1. 編寫單元測試保持(Preserve)程序行為;2. 重構代碼;3. 保證單元測試通過。 重構 還引入了一套重構術語(諸如封裝字段,內聯方法,和字段上移),以便程序員之間交流。只有理解了這三個方面,才能算是理解重構。

修改代碼的藝術

這裏再重復一遍重構的定義——在保持代碼行為的基礎上,提升代碼的質量。 重構 專註於第二步,即如何提升代碼的質量,而 修改代碼的藝術 專註於第一步,即如何保持代▓碼的行為。

提升代碼質量並不困難,但保持代碼行為就難多了,尤其是對沒有測試的遺留代碼(Legacy Code)而言——你需要首先引入測試,但遺留代碼往往可測試性(Testability)很差,這時你就需要把代碼變的可測試。 修改代碼的藝術 包含大量的實用建議,用來把代碼變的可測試(Testable),從而使重構變為可能,使提高代碼質量變為可能。

延伸閱讀:

  • 重構與模式 :這本書的中文書名存在誤導,它的原書書▓名是 Refactoring to Patterns——通過重構,把模式引入代碼。這本書闡述了重構和設計模式之間的關系,使得程序員可以在更高的層次上思考重構,進行重構。

4. 軟件測試

How to Break Software

關於軟件測試的書籍很多,但很少有一本測試書籍能像 How to Break Software 這般既有趣又實用。不同於傳統的軟件▓測試書籍(往往空話連篇,無法直接應用), How to Break Software 非常實際——它從程序員的心理出發,分析軟件錯誤/Bug最可能產生的路徑,然後針對這些路徑進行 殘酷 的測試,以保證軟件質量。

我在第一次閱讀這本書時大呼作者太過“殘忍”——連這些刁鉆詭異的測試招數都能想出來。但這種毫不留情(Relentless)的測試風格正是每個專業程序員所應具備的心態。

註意:如果你是一個測試工程師,那麽在閱讀這本書前請三思——因為閱讀它之後你會讓你身邊的程序員苦不堪言,甚至連掐死你的心都有 :-D。

xUnit Test Patterns

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/360OS/360 這兩個即是放到現在也是巨型軟件項目的裏程碑項目▓的經驗總結。它覆蓋了軟件項目各個方面的關鍵概念:從工期管理( Brooks定律 )到團隊建設( 外科團隊 ),從程序設計(編程的本質是使用正確的數據結構)到架構設計( 概念完整性 ),從原型設計(Plan to Throw one away)到團隊交流(形式化▓文檔+會議)。令人驚訝的是,即便40年之後, 人月神話 中的關鍵概念(包括焦▓油坑, Brooks定律概念完整性外科團隊第二版效應 等等)依然適用,而軟件開發的 核心復雜度 仍然沒有得到解決( 沒有銀彈 )。

延伸閱讀:

  • 人件(原書第3版) :從人的角度分析軟件項目。 人件 從雇傭正確的人,創建健康的工作環境,以及打造高效的開發團隊等角度闡述了如何改善人,從而改善軟件項目;
  • 門後的秘密:卓越管理的故事 :這本書生動的再現了軟件項目管理工作的場景,並給出了各種實用管理技巧,如果你有意轉向管理崗位,這本書不容錯過;
  • 大教堂與集市 :這本書從黑客的█歷史說起,系統而又風趣的講述了開源運動的理論和實踐,以及開源軟件項目是█如何運作並發展的。了解開源,從這本書開始。

6. 專業開發

程序員修煉之道:從小工到專家

不要被庸俗的譯名迷惑, 程序員修煉之道 是一本價值極高的程█序員成長手冊。這本書並不局限於特定的編程語言或框架,而是提出了一套切實可行的實效(Pragmatic)開發哲學,並通過程序設計,測試,編程工具,以及項目管理等方面的實例展示了如何應用這套開發哲學,從而使得程序員更加高效專業。有人把這本書稱之為迷你版 代碼大全 —— 代碼大全 給出了大量的優秀程序設計實踐,偏向術;而 程序員修煉之道 給出了程序設計實踐背後的思想,註重道。

程序員職業素養

程序員修煉之道 指出了如何成為專業程序員,這本 程序員職業素養 則指出了專業程序員應該是什麽樣子——承擔責任;知道自己在做什麽;知道何時說不/何時說是;在正確的時間編寫正確的代碼;懂得自我時間管理和工期預估;知道如何應對壓力。如果你想成為專業程序員(Professional Developer)(而不是碼農(Code Monkey)),這本書會為你指明前進的方向。

延伸閱讀:

7. 大師之言

奇思妙想:15 位計算機天才及其重大發現

奇思妙想:15 位計算機天才及其重大發現 是一本極具眼光的技術訪談書籍——在這本書訪談的 15 位計算機科學家中,竟出現了 12 位 圖靈獎 獲得者——要知道圖靈獎從 1966 年設獎到現在也只有六十幾位獲獎者而已。

奇思妙想 把計算機科學分為四大領域:編程語言;算法;架構;人工智能。並選取了每個領域下最具代表性的計算機科學家進行訪談。因為這些計算機科學家都是其所在領域的開拓者,因此他們能給出常人無法給出的深刻見解。通過這本書,你可以了解前三十年的計算機科學的發展歷程——計算機科學家做了什麽,而計算機又能做到/做不到什麽。從而避免把時間浪費█在前人已經解決的問題(或者根本無法解決的問題)上面。

編程人生:15位軟件先驅訪談錄

同樣是訪談錄,同樣訪談 15 個人, 編程人生 把重點放在程序員(Coders at work)上。它從各個領域選取了15位頂尖的程序員,這些程序員既包括 Ken ThompsonJamie Zawinski 這些老牌Unix黑客,也包括 Brad Fitzpatrick 這樣的80後新生代,還包括 Frances AllenDonald Knuth 這樣的計算機科學家。這種多樣性(Diversity)使得 編程人生 兼具嚴謹性和趣味性,無論你是什麽類型的程序員,都能從中受益良多。

延伸閱讀:

  • 圖靈和 ACM 圖靈獎(1966-2011) :通過圖靈獎介紹整個計算機科學發展史,非常難得的國產精品圖書;
  • 編程大師訪談錄 :可以把這本書看作為二十年前的 編程人生 ,被訪談者都是當時叱咤風雲的人物(例如微軟的創造者 Bill Gates ,Macintosh 的發明者 Jeff Raskin ,以及 Adobe 的創始人 John Warnock 等等)。有趣的是這本書中大量的經驗和建議到如今依然適用;
  • 編程大師智慧 :類似於 編程人生 ,不同的是被訪談者都是編程語言的設計者——這本書覆蓋了除C語言以外的幾乎所有主流編程語言。通過這本書,你可以從中學到編程語言背後的設計思想——編程語言為什麽要被設計成這樣,是什麽促使設計者要在語言中加入這個特性(或拒絕那個特性)。從而提升對編程語言的理解。

8. 界面設計

寫給大家看的設計書

書如其名, 寫給大家█看的設計書 是一本面向初學者的快速設計入門。它覆蓋了版式,色彩,和字體這三個設計中的關鍵元素,並創造性的為版式設計總結出CRAP四大原則(Contrast 對比,Repetition 重復,Alignment 對齊,Proximity 親密)。全書使用豐富生動的範例告訴讀者什麽是好的設計,什麽是不好的設計,使得即便是對設計一無所知的人,也可以從這本書快速入門。

認知與設計:理解UI設計準則(第 2 版)

寫給大家看的設計書 強調實踐,即如何做出好的設計; 認知與設計:理解 UI 設計準則 強調理論,即為什麽我們會接受這樣的設計而反感那樣的設計。如果你想要搞清楚設計背後的心理學知識,但又不想閱讀大部頭的心理學著作,那麽 認知與設計 是你的首選。

延伸閱讀:

  • GUI 設計禁忌 2.0 :這本書指出了 GUI 設計的原則和常見誤區,然後通過具體範例指出了如何避免這些誤區。如果你的工作涉及到用戶界面,那麽這本書會為你減少很多麻煩;
  • 界面設計模式(第 2 版) :這本書將用戶界面中的常見元素/行為組織成彼此關聯的模式,以便讀者理解並舉一反三,從而將其運用到自己的應用中;
  • 移動應用 UI 設計模式 :類似於 界面設計模式 ,但面向移動平臺。它給出了 iOS,Android,以及Windows Phones 上常用的 90 余種界面設計模式,從而使得你不必把這些平臺的應用挨個玩一遍也可以掌握各個平臺的設計精髓。如果你主攻 Android 平臺,那麽 Android 應用 UI 設計模式 會是更好的選擇;
  • 配色設計原理版式設計原理 :如果你讀過 寫給大家看的設計書 之後想繼續深入學習設計,這兩本書是不錯的起點。

9. 交互設計

通用設計法則

書如其名, 通用設計法則 給出了重要的 125 個設計原則,並用簡練的語言和範例展示了這些原則的實際應用。每個原則都有對應的參考文獻,以便讀者進一步學習。我之所以推薦這本書,是因為:1. 程序員需要對設計有全面的認識;2. 程序員並不需要知道這些設計原則是怎麽來的,知道怎麽用即可。這本書很好的滿足了這兩個要求。

交互設計精髓(第3版)

交互設計精髓 是交互設計領域的聖經級著作。交互設計專家(以及 VB 之父) Alan Cooper 在這本書中詳細介紹了交互設計的原則,流程,以及方法,然後通過各種範例(主要來▓自桌面系統)展示了如何應用這些原則。

需要註意的是這本書的 第 4 版 已經出版,它在第三版的基礎上增加了移動設計以及 Web 設計等內容。

延伸閱讀:

  • The Design of Everyday Things :交互設計領域的另一本經典之作,它通過解讀人類行動背後的心理活動,展示了設計問題的根源,並給出了一系列方法用以解決設計問題(需要註意,盡管這本書有中譯版,但中譯版對應的是 02 年的舊版,而非13年的新版);
  • The Inmates Are Running the AsylumAlan Cooper 的另一本經典,這本書非常辛辣的指出讓不具備人機交互知識的程序員直接編寫面向用戶的軟件就像讓精神病人管理瘋人院(The Inmates Are Running the Asylum),然後給出了一套交互設計流程以挽救這個局面;
  • 簡約至上:交互式設計四策略 :專註於把產品變的更加簡單易用。作者通過刪除,組織,隱藏,和轉移▓這四個策略,展示了如何█創造出簡約優質的用戶體驗。

個人成長

1. 職業規劃

軟件開發者路線圖

軟件開發者路線圖 是一本優秀且實用的程序員職業規劃手冊。這本書由若幹個模式組成,每個模式都對應於程序員職業生涯中的特定階段。通過這本書,讀者可以很方便的找到自己目前所處的模式(階段),應該做什麽,目標是什麽,以及下一個模式(階段)會是什麽。如果你時常感到迷茫,那麽請閱讀這本 路線圖 ,找到自己的位置,確定接下來的方向。

延伸閱讀:

  • 卡耐基全集 :非常著名的為人處世書籍。很多人把這本書歸類到成功學,但我並不這麽認為——在我看來,這本書教的更多的是如何成為一個讓大家喜▓歡的人。作為天天和機器打交道的程序員,這套書會幫助我們與人打交道;
  • 沃頓商學院最受歡迎的談判課 :這本書不是教你去談判,而是教你通過談判(Negotiation)去得到更多(Getting more,這也是這本書的原書書名)。小到買菜砍價,大到爭取項目,這本書中的談判原則會讓你收益良多;
  • 程序員健康指南 :作為長期與計算機打交道的職業,程序員往往會受到各式各樣疾病的困擾,這本書正是為了解決這個問題而出現:它從改善工作環境,調整飲食結構,預防頭痛眼痛,以及進行室內/室外鍛煉等方面出發,給出了一套全面且可行的程序員健康改善計劃,以幫助程序員打造健康的身體。

2. 思維方式

程序員的思維修煉:開發認知潛能的九堂課

作為程序員,我們需要不斷地學習——既要學習新技術,也要學習如何解決各種領域的問題。為了提升學習效率,我們需要學習 如何學習程序員的思維修煉 正是這樣一本講如何學習的書,它集合了認知科學,神經學,以及行為理論的最新研究成果,並系統的介紹了大腦的工作機制。通過這本書,你將學會如何高效的使用自己的大腦,從而提高思考能力,改善學習效率。

如何把事情做到最好

Mastery is not about perfection. It’s about a process, a journey. The master is the one who stays on the path day after day, year after 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 to Better Ideas :與批判性思維相反,這本書專註於創造性思維(Creative Thinking),它分析了阻礙創造性思維的常見思維障礙(Blockbuster)以及這些思維障礙背後的成因,並給出了各種方法以破除這些障礙。

3. 求職面試

金領簡歷:敲開蘋果微軟谷歌的大門

知己知彼,百戰不殆。 金領簡歷:敲開蘋果微軟谷歌的大門 是程序員求職的必讀書籍,它覆蓋了程序員求職的方方面面:從開始準備到編寫簡歷,從技術面試到薪酬談判。由於該書作者曾在 Google,微軟,和蘋果任職並進行過技術招聘,因此這本書的內容非常實用。

順便吐個槽:這本書翻譯的還不錯,但我實在無法理解封面上的“進入頂級科技公司的葵花寶典”這段文字——找個工作而已,用不著切JJ這麽兇殘吧。-_-#

程序員面試金典(第 5 版)

同樣是來自 金領簡歷 作者的作品, 程序員面試金典(第 5 版) 專註於技術面試題,它既包含了 IT 企業(諸如微軟,Google,和蘋果)的面試流程以及如何準▓備技術面試,也包含了大量(超過200道)常見技術面試題題目以及解題思路。無論你打算進入國內企業還是外企,你都應該把這本書的題目練一遍,以找到技術面試的感覺(我在求職時就曾經專門搞了一塊白板,然後每二十分鐘一道題的練習,效果很不錯)。

延伸閱讀:

  • 編程之美:微軟技術面試心得 :恐怕是國內技術面試第一書,這本書裏面的多數題目都曾經是國內IT企業面試的必問題目。這本書的缺點是它太舊而且被用濫了(以至於一些企業開始避免使用這本書上的題目)——但你可以把它當成一本算法趣題來讀;
  • 劍指 Offer:名企面試官精講典型編程題 :相對於東拼西湊的XX面試寶典, 劍指Offer 是一本少見的國產精品技術面試書籍,盡管這本書的技術面試題目不多(60 余道),但作者為大多數題目都給出了不同方式的解法,並分析了這些解法之間的優劣,此外作者還以面試官的視角分析了技術面試的各個環節,從而幫助讀者把握技術面試;
  • 人人都有好工作:IT 行業求職面試必讀 :可以把它看做 金領簡歷 的補充閱讀——這本書的特點在於它給出了非常詳細的簡歷/求職信/電子郵件編寫技巧,而這正是不少國內程序員所缺乏的。

4. 英語寫作

The Only Grammar Book You'll Ever Need

詞匯量決定閱讀能力,語法決定寫作能力。計算機專業詞匯並不多,但精確性非常重要,因此每個程序員都應具備良好的英語語法,但程序員並不需要過於專業的英語語法——掌握常用語法並把它用對就可以。 The Only Grammar Book You’ll Ever Need 正好可以滿足這個需求,盡管它篇幅不大(不足 200 頁),卻覆蓋了英語中的關鍵語法以及常見錯誤。把這本書讀兩遍,它會大幅度提高你的英語寫作能力。

風格的要素

既是最暢銷的英語寫作書籍,也是計算機書籍中引用最多的非計算機書籍。 風格的要素 用極其簡練的語言講述了如何進行 嚴肅精確清楚 的英語寫作。從這本書中,你不僅可以學到英語寫作,更可以學到一種嚴謹至簡的處事態度,而這正是專業開發所必需的。

延伸閱讀:

  • 牛津英語用法指南(第 3 版) :全面且權威的英語用法指南,它覆蓋語法,詞匯,發音,以及修辭等方面,並兼顧口語和書面語,以幫助讀者掌握合理的英語用法(Proper English Usage)。不要被這本書的篇幅(1000 多頁)嚇到——原書並沒有這麽厚,因為這本書被翻譯成中文但又得保留原有的英文內容,所以它的篇幅幾乎翻了一倍。考慮到這本書使用的詞匯都很基礎,所以我認為具有英語基礎的讀者直接閱讀原版( Practical English Usage )會更合適;
  • 寫作法寶:非虛構寫作指南(30周年紀念版) :詳盡的非虛構(Non-Fiction)寫作指南,無論你要寫地方,技術,商務,運動,藝術,還是自傳,你都可以從這本書中找到珍貴的建議;
  • 中式英語之鑒 :中國人使用英語最大的問題就是會把中式思維摻雜其中,從而形成啰裏啰嗦不倫不類的中式英語(Chinglish)。 中式英語之鑒 系統的探討了中式英語以及其成因,然後根據成因對中式英語進行歸類,並對每個類別給出了大量的實際案例以及修改建議。如果你想擺脫中式英語,那麽這本書是絕好的起點。

如何使用這個書單

學而不思則罔,思而不學則殆。

不憤不啟,不悱不發。舉一隅不以三隅反,則不復也。

不聞不若聞之,聞之不若見之,見之不若知之,知之不若行之,學至於行之而止矣。

來自他人的書單

它山之石,可以攻玉。我在本文最後給出其他中外優秀程序員的書單,以便參考&補充。

劉未鵬(暗時間作者)

以下同一條目下用“/”隔開的表示任選,當然也可以都讀。

  1. 編碼:隱匿在計算機軟硬件背後的語言
  2. 深入理解計算機系統 / Windows 核心編程 / 程序員的自我修養
  3. 代碼大全 / 程序員修煉之道
  4. 編程珠璣 / 算法概論 / 算法設計 / 編程之美
  5. C 程序設計語言
  6. C++ 程序設計語言 / C++ 程序設計原理與實踐 / Accelerated C++
  7. 計算機程序的構造與解釋
  8. 代碼整潔之道 / 實現模式
  9. 設計模式 / 敏捷軟件開發(原則模式與實踐)
  10. 重構

雲風(中國遊戲編程先行者,前網易遊戲部門資深程序員,簡悅創始人)

  1. C++ 編程思想
  2. Effective C++
  3. 深度探索 C++ 對象模型
  4. C++ 語言的設計與演化
  5. C 專家編程
  6. C 陷阱與缺陷
  7. C 語言接口與實現
  8. Lua 程序設計
  9. 鏈接器和加載器
  10. COM 本質論
  11. Windows 核心編程
  12. 深入解析 Windows 操作系統
  13. 程序員修煉之道
  14. 代碼大全
  15. UNIX 編程藝術
  16. 設計模式
  17. 代碼優化:有效使用內存
  18. 深入理解計算機系統
  19. 深入理解 LINUX 內核
  20. TCP/IP 詳解

洪強寧(豆瓣技術總監)

  1. 代碼大全
  2. 人月神話
  3. 編碼:隱匿在計算機軟硬件背後的語言
  4. 計算機程序設計藝術
  5. 程序員修煉之道
  6. 設計模式
  7. 計算機程序的構造與解釋
  8. 重構
  9. C 程序設計語言
  10. 算法導論

陳皓(CoolShell博主)

  1. 點石成金:訪客至上的 Web 和移動可用性設計秘笈
  2. 重來:更為簡單有效的商業思維
  3. 黑客與畫家
  4. 清醒思考的藝術
  5. TCP/IP 詳解
  6. UNIX 環境高級編程
  7. UNIX 網絡編程

張崢(微軟亞洲研究院副院長)

  1. 算法概論
  2. Data Structure and Algorithms
  3. C 程序設計語言
  4. UNIX 操作系統設計
  5. 編譯原理
  6. 計算機體系結構:量化研究方法
  7. 當下的幸福
  8. 異類:不一樣的成功啟示錄

Jeff Atwood(Stackoverflow聯合創始人)

  1. 代碼大全
  2. 人月神話
  3. 點石成金:訪客至上的Web和移動可用性設計秘笈
  4. 快速軟件開發
  5. 人件
  6. The Design of Everyday Things
  7. 交互設計精髓
  8. The Inmates Are Running the Asylum
  9. GUI設計禁忌 2.0
  10. 編程珠璣
  11. 程序員修煉之道
  12. 精通正則表達█式

Joel Spolsky(Stackoverflow聯合創始人)

軟件項目管理

  1. 人件
  2. 人月神話
  3. 快速軟件開發

編程技藝

  1. 代碼大全
  2. 程序員修煉之道

編程哲學

  1. 禪與摩托車維修藝術
  2. 哥德爾、艾舍爾、巴赫:集異璧之大成
  3. 建築模式語言

界面設計

  1. 點石成金:訪客至上的 Web 和移動可用性設計秘笈
  2. 交互設計精髓
  3. The Design of Everyday Things

資本運作

  1. 漫步華爾街

圖形設計

  1. 寫給大家看的設計書

思維方式

  1. 影響力
  2. Helplessness On Depression, Development and Death

編程入門

  1. 編碼:隱匿在計算機軟硬件背後的語言
  2. C 程序設計語言

DHH(Ruby on Rails創始人)

  1. Smalltalk Best Practice Patterns
  2. 重構
  3. 企業應用架構模式
  4. 領域驅動設計
  5. 你的燈亮著嗎?發現問題的真正所在

參考

  1. 怎樣花兩年時間去面試一個人
  2. What is the single most influential book every programmer should read?
  3. Recommended Reading for Developers
  4. Book Reviews – Joel Spolsky
  5. The 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; conference a ready man; and writing an exact 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 全程指南 /blog/sublime-text-complete-guide/ 2014-09-27T20:57:15.000Z 2019-10-21T05:00:04.739Z 摘要(Abstract)

本文系統全面的介紹了 Sublime Text,旨在成為最優秀的 Sublime Text 中文教程。

更新記錄

  1. 2014/09/27:完成初稿
  2. 2014/09/28:
    • 更正打開控制臺的快捷鍵為 Ctrl + `
    • 更正全局替換的快捷鍵為 Ctrl + Alt + Enter
  3. 2016/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,原因有下面幾點:

  1. 跨平臺:Vim 和 Sublime Text 均為跨平臺編輯器(在 Linux、OS X 和 Windows 下均可使用)。作為一個程序員,切換系統是常有的事情,為了減少重復學習,使用一個跨平臺的編輯器是很有必要的。
  2. 可擴展:Vim 和 Sublime Text 都是可擴展的(Extensible),並包含大量實用插件,我們可以通過安裝自己領域的插件來成倍提高工作效率。
  3. 互補: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 集成開發環境(Integrated Development Environment,下文簡稱 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 打開 “系統屬性”。

sysdm.cpl

然後在 “高級” 選項卡裏選擇 “環境變量”,編輯 “Path”,增加 Sublime Text 的安裝目錄(例如 D:\Program Files\Sublime Text 3)。

添加環境變量

接下來你就可以在命令行裏面利用 subl 命令直接使用 Sublime Text 了:

1
2
3
subl 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 控制臺。
  • 將下面的代碼粘貼到控制臺裏:
1
import 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 manual 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 安裝成功

成功安裝 Package Control 之後,我們就可以方便的安裝使用 Sublime Text 的各種插件:

使用 Package Control 安裝插件

購買(Purchase)

Sublime Text 是一個收費閉源軟件,這在一定程度上成為了我支持 Sublime Text 的理由(我心中的軟件靠譜程度:免費開源 << 免費閉源 < 收費開源 < 收費閉源):在 這裏 購買。

不過不購買 Sublime Text 也可以 “正常” 使用它,只是 Sublime Text 會時不時的彈出一個對話框提醒你購買,此外窗口處會有一個很屌絲很 low 逼的 (UNREGISTERED)。(在高頻操作下,一般 20 分鐘提示一次,個人認為算是很厚道了)

提示註冊

也許不少人會覺著 Sublime Text 70 刀的價格太貴,但相比它的功能和帶來的效率提升,70 刀真的不值一提,如果你不方便使用 Paypal 付款可以郵件聯系我,你支付寶給我打款然後我幫你付款,價格按當日匯率折算(450 元左右)。

購買之後

概覽(Tour)

基本概念(Basic Concepts)

Sublime Text 的界面如下:

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 配置文件,例如:

1
2
3
4
{
"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 調出搜索框進行搜索:

Sublime Text的搜索框

以及使用 Ctrl + H 進行替換:

Sublime Text的替換框

關鍵字查找&替換

對於普通用戶來說,常規的關鍵字搜索就可以滿足其需求:在搜索框輸入關鍵字後 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)),以及在線測試工具(regexpalregexer)。

多文件搜索&替換

使用 Ctrl + Shift + F 開啟多文件搜索&替換(註意此快捷鍵和搜狗輸入法的簡繁切換快捷鍵有沖突):

多文件搜索界面

多文件搜索&替換默認在當前打開的文件和文件夾進行搜索/替換,我們也可以指定文件/文件夾進行搜索/替換。

跳轉(Jumping)

Sublime Text 提供了強大的跳轉功能使得我們可以在不同的文件/方法/函數中無縫切換。就我的使用經驗而言,目前還沒有哪一款編輯器可以在這個方面超越Sublime Text。

跳轉到文件

Ctrl + P 會列出當前打開的文件(或者是當前文件夾的文件),輸入文件名然後 Enter 跳轉至該文件。

需要註意的是,Sublime Text使用模糊字符串匹配(Fuzzy String Matching),這也就意味著你可以通過文件名的前綴、首字母或是某部分進行匹配:例如, EISEclipStupid 都可以匹配 EclipseIsStupid.java

跳轉到文件

跳轉到符號

盡管是一個文本編輯器,Sublime Text 能夠對代碼符號進行一定程度的索引。 Ctrl + R 會列出當前文件中的符號(例如類名和函數名,但無法深入到變量名),輸入符號名稱 Enter 即可以跳轉到該處。此外,還可以使用 F12 快速跳轉到當前光標所在符號的定義處(Jump to Definition)。

跳轉到符號

比較有意思的是,對於 Markdown, Ctrl + R 會列出其大綱,非常實用。

Markdown大綱

跳轉到某行

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 支持以文件夾做為單位進行編輯,這在編輯一個文件夾下的代碼時尤其有用。在 FileOpen Folder

文件夾視圖

你會發現右邊多了一個側欄,這個側欄列出了當前打開的文件和文件夾的文件,使用 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++ 默認界面

Notepad++

Sublime Text 默認界面

Sublime Text

所以在用過 Sublime Text 之後,我立刻就卸掉了 Notepad++。

Sublime Text 自帶的風格是我喜歡的深色風格(也可以調成淺色),默認主題是Monokai Bright,這兩者的搭配已經很不錯了,不過我們還可以做得更好:接下來我將會展示如何通過設置偏好項和添加自定義風格/主題使得 Sublime Text 更加 Stylish。

一些設置(Miscellaneous Settings)

下面是我個人使用的設置項。

1
2
3
4
5
6
7
8
9
// 設置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 Light

淺色版

Soda Dark

深色版

Nexus

Nexus

Flatland

Flatland

Spacegray Light

淺色版

Spacegray Dark

深色版

配色(Color)

colorsublime 包含了大量 Sublime Text 配色方案,並支持在線預覽,配色方案的安裝教程在 這裏,恕不贅述。

我個人使用的是 Nexus 主題和 Flatland Dark 配色,配置如下:

1
2
"theme": "Nexus.sublime-theme",
"color_scheme": "Packages/Theme - Flatland/Flatland Dark.tmTheme",

效果如下:

Nexus+Flatland

編碼(Coding)

優秀的編輯器使編碼變的更加容易,所以 Sublime Text 提供了一系列功能以提高開發效率。

良好實踐(Good Practices)

良好的代碼應該是規範的,所以Google為每一門主流語言都設置了其代碼規範(Code Style Guideline)。我自己通過下面的設置使以規範化自己的代碼。

1
2
3
4
5
6
7
8
9
10
11
12
// 設置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 可以以當前縮進粘貼代碼(非常實用)。

除了手動格式化,我們也可以通過安裝插件實現自動縮進和智能對齊:

自動完成(Auto Completion)

Sublime Text 支持一定的自動完成,按 Tab 自動補全。

自動完成

括號(Brackets)

編寫代碼時會碰到大量的括號,利用 Ctrl + M 可以快速的在起始括號和結尾括號間切換, Ctrl + Shift + M 則可以快速選擇括號間的內容,對於縮進型語言(例如Python)則可以使用 Ctrl + Shift + J

括號演示

此外,我使用 BracketHighlighter 插件以高亮顯示配對括號以及當前光標所在區域,效果如下:

插件演示

命令行(Command Line)

盡管提供了 Python 控制臺,但 Sublime Text 的控制臺僅支持單行輸入,十分不方便,所以我使用 Sublime?REPL 以進行一些編碼實驗(Experiments)。

SublimeREPL演示

其它(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)

鏈接(Links)

  • 官方文檔:http://www.sublimetext.com/docs/3/
  • 官方論壇:
  • Stack Overflow 的 Sublime Text 頻道:
  • 非官方文檔: 甚至比官方文檔還要全面!
  • Package Control: 大量的 Sublime Text 插件和主題。

視頻(Videos)

以上。

]]>
<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簡介 /blog/an-introduction-to-swift/ 2014-06-03T01:22:19.000Z 2019-10-21T05:00:04.739Z 關於

這篇文章簡要介紹了蘋果於 WWDC 2014 發布的編程語言——Swift。

前言

在這裏我認為有必要提一下 Bret VictorInventing 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 best of C and Objective-C, without the constraints of C compatibility.

Swift adopts safe programming patterns and adds modern features to make programming easier, more flexible and more fun.

Swift’s clean slate, backed by the mature and much-loved Cocoa and Cocoa Touch frameworks, is an opportunity to imagine how software development works.

Swift is the first industrial-quality systems programming language that is as expressive and enjoyable as a scripting language.

簡單的說:

  1. Swift 用來寫 iOS 和 OS X 程序。(估計也不會支持其它屌絲系統)
  2. Swift 吸取了 C 和 Objective-C 的優點,且更加強大易用。
  3. Swift 可以使用現有的 Cocoa 和 Cocoa Touch 框架。
  4. Swift 兼具編譯語言的高性能(Performance)和腳本語言的交互性(Interactive)。

Swift語言概覽

基本概念

註:這一節的代碼源自 The Swift Programming Language 中的 A Swift Tour

Hello, world

類似於腳本語言,下面的代碼即是一個完整的Swift程序。

1
println("Hello, world")

變量與常量

Swift使用 var 聲明變量,let 聲明常量。

1
2
3
var myVariable = 42
myVariable = 50
let myConstant = 42

類型推導

Swift支持類型推導(Type Inference),所以上面的代碼不需指定類型,如果需要指定類型:

1
let explicitDouble : Double = 70

Swift不支持隱式類型轉換(Implicitly casting),所以下面的代碼需要顯式類型轉換(Explicitly casting):

1
2
3
let label = "The width is "
let width = 94
let labelWidth = label + String(width)

字符串格式化

Swift 使用 \(item) 的形式進行字符串格式化:

1
2
3
4
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

數組和字典

Swift 使用 [] 操作符聲明數組(array)和字典(dictionary):

1
2
3
4
5
6
7
8
var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"

一般使用初始化器(initializer)語法創建空數組和空字典:

1
2
let emptyArray = String[]()
let emptyDictionary = Dictionary<String, Float>()

如果類型信息已知,則可以使用 [] 聲明空數組,使用 [:] 聲明空字典。

控制流

概覽

Swift的條件語句包含 ifswitch,循環語句包含 for-inforwhiledo-while,循環/判斷條件不需要括號,但循環/判斷體(body)必需括號:

1
2
3
4
5
6
7
8
9
let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
if score > 50 {
teamScore += 3
} else {
teamScore += 1
}
}

可空類型

結合 iflet,可以方便的處理可空變量(nullable variable)。對於空值,需要在類型聲明後添加 ? 顯式標明該類型可空。

1
2
3
4
5
6
7
8
var optionalString: String? = "Hello"
optionalString == nil
var optionalName: String? = "John Appleseed"
var gretting = "Hello!"
if let name = optionalName {
gretting = "Hello, \(name)"
}

靈活的switch

Swift 中的 switch 支持各種各樣的比較操作:

1
2
3
4
5
6
7
8
9
10
11
let 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 除了遍歷數組也可以用來遍歷字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let interestingNumbers = [
"Prime": [2, 3, 5, 7, 11, 13],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (kind, numbers) in interestingNumbers {
for number in numbers {
if number > largest {
largest = number
}
}
}
largest

while 循環和 do-while 循環:

1
2
3
4
5
6
7
8
9
10
11
var n = 2
while n < 100 {
n = n * 2
}
n
var m = 2
do {
m = m * 2
} while m < 100
m

Swift 支持傳統的 for 循環,此外也可以通過結合 ..(生成一個區間)和 for-in 實現同樣的邏輯。

1
2
3
4
5
6
7
8
9
10
11
var firstForLoop = 0
for i in 0..3 {
firstForLoop += i
}
firstForLoop
var secondForLoop = 0
for var i = 0; i < 3; ++i {
secondForLoop += 1
}
secondForLoop

註意:Swift 除了 .. 還有 ..... 生成前閉後開的區間,而 ... 生成前閉後閉的區間。

函數和閉包

函數

Swift 使用 func 關鍵字聲明函數:

1
2
3
4
func greet(name: String, day: String) -> String {
return "Hello \(name), today is \(day)."
}
greet("Bob", "Tuesday")

通過元組(Tuple)返回多個值:

1
2
3
4
func getGasPrices() -> (Double, Double, Double) {
return (3.59, 3.69, 3.79)
}
getGasPrices()

支持帶有變長參數的函數:

1
2
3
4
5
6
7
8
9
func sumOf(numbers: Int...) -> Int {
var sum = 0
for number in numbers {
sum += number
}
return sum
}
sumOf()
sumOf(42, 597, 12)

函數也可以嵌套函數:

1
2
3
4
5
6
7
8
9
func returnFifteen() -> Int {
var y = 10
func add() {
y += 5
}
add()
return y
}
returnFifteen()

作為頭等對象,函數既可以作為返回值,也可以作為參數傳遞:

1
2
3
4
5
6
7
8
func makeIncrementer() -> (Int -> Int) {
func addOne(number: Int) -> Int {
return 1 + number
}
return addOne
}
var increment = makeIncrementer()
increment(7)
1
2
3
4
5
6
7
8
9
10
11
12
13
func 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 中可以利用 {} 聲明匿名閉包:

1
2
3
4
5
numbers.map({
(number: Int) -> Int in
let result = 3 * number
return result
})

當閉包的類型已知時,可以使用下面的簡化寫法:

1
numbers.map({ number in 3 * number })

此外還可以通過參數的位置來使用參數,當函數最後一個參數是閉包時,可以使用下面的語法:

1
sort([1, 5, 3, 12, 2]) { $0 > $1 }

類和對象

創建和使用類

Swift 使用 class 創建一個類,類可以包含字段和方法:

1
2
3
4
5
6
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}

創建 Shape 類的實例,並調用其字段和方法。

1
2
3
var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

通過 init 構建對象,既可以使用 self 顯式引用成員字段(name),也可以隱式引用(numberOfSides)。

1
2
3
4
5
6
7
8
9
10
11
12
class 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 父類方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class 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 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class 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.perimeter
triangle.perimeter = 9.9
triangle.sideLength

註意:賦值器(setter)中,接收的值被自動命名為 newValue

willSet和didSet

EquilateralTriangle 的構造器進行了如下操作:

  1. 為子類型的屬性賦值。
  2. 調用父類型的構造器。
  3. 修改父類型的屬性。

如果不需要計算屬性的值,但需要在賦值前後進行一些操作的話,使用 willSetdidSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class 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.sideLength
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
triangleAndSquare.triangle.sideLength

從而保證 trianglesquare 擁有相等的 sideLength

調用方法

Swift中,函數的參數名稱只能在函數內部使用,但方法的參數名稱除了在內部使用外還可以在外部使用(第一個參數除外),例如:

1
2
3
4
5
6
7
8
class Counter {
var count: Int = 0
func incrementBy(amount: Int, numberOfTimes times: Int) {
count += amount * times
}
}
var counter = Counter()
counter.incrementBy(2, numberOfTimes: 7)

註意Swift支持為方法參數取別名:在上面的代碼裏,numberOfTimes 面向外部,times 面向內部。

?的另一種用途

使用可空值時,? 可以出現在方法、屬性或下標前面。如果 ? 前的值為 nil,那麽 ? 後面的表達式會被忽略,而原表達式直接返回 nil,例如:

1
2
3
let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional
square")
let sideLength = optionalSquare?.sideLength

optionalSquarenil時,sideLength屬性調用會被忽略。

枚舉和結構

枚舉

使用 enum 創建枚舉——註意Swift的枚舉可以關聯方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Rank: Int {
case Ace = 1
case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King
func simpleDescription() -> String {
switch self {
case .Ace:
return "ace"
case .Jack:
return "jack"
case .Queen:
return "queen"
case .King:
return "king"
default:
return String(self.toRaw())
}
}
}
let ace = Rank.Ace
let aceRawValue = ace.toRaw()

使用 toRawfromRaw 在原始(raw)數值和枚舉值之間進行轉換:

1
2
3
if let convertedRank = Rank.fromRaw(3) {
let threeDescription = convertedRank.simpleDescription()
}

註意枚舉中的成員值(member value)是實際的值(actual value),和原始值(raw value)沒有必然關聯。

一些情況下枚舉不存在有意義的原始值,這時可以直接忽略原始值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Suit {
case Spades, Hearts, Diamonds, Clubs
func simpleDescription() -> String {
switch self {
case .Spades:
return "spades"
case .Hearts:
return "hearts"
case .Diamonds:
return "diamonds"
case .Clubs:
return "clubs"
}
}
}
let hearts = Suit.Hearts
let heartsDescription = hearts.simpleDescription()

除了可以關聯方法,枚舉還支持在其成員上關聯值,同一枚舉的不同成員可以有不同的關聯的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum ServerResponse {
case Result(String, String)
case Error(String)
}
let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Error("Out of cheese.")
switch success {
case let .Result(sunrise, sunset):
let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
case let .Error(error):
let serverResponse = "Failure... \(error)"
}

結構

Swift 使用 struct 關鍵字創建結構。結構支持構造器和方法這些類的特性。結構和類的最大區別在於:結構的實例按值傳遞(passed by value),而類的實例按引用傳遞(passed by reference)。

1
2
3
4
5
6
7
8
9
struct 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 定義協議:

1
2
3
4
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}

類型、枚舉和結構都可以實現(adopt)協議:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class 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.simpleDescription
struct SimpleStructure: ExampleProtocol {
var simpleDescription: String = "A simple structure"
mutating func adjust() {
simpleDescription += " (adjusted)"
}
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription

擴展

擴展用於在已有的類型上增加新的功能(比如新的方法或屬性),Swift 使用 extension 聲明擴展:

1
2
3
4
5
6
7
8
9
extension Int: ExampleProtocol {
var simpleDescription: String {
return "The number \(self)"
}
mutating func adjust() {
self += 42
}
}
7.simpleDescription

泛型(generics)

Swift 使用 <> 來聲明泛型函數或泛型類型:

1
2
3
4
5
6
7
8
func repeat<ItemType>(item: ItemType, times: Int) -> ItemType[] {
var result = ItemType[]()
for i in 0..times {
result += item
}
return result
}
repeat("knock", 4)

Swift 也支持在類、枚舉和結構中使用泛型:

1
2
3
4
5
6
7
// Reimplement the Swift standard library's optional type
enum OptionalValue<T> {
case None
case Some(T)
}
var possibleInteger: OptionalValue<Int> = .None
possibleInteger = .Some(100)

有時需要對泛型做一些需求(requirements),比如需求某個泛型類型實現某個接口或繼承自某個特定類型、兩個泛型類型屬於同一個類型等等,Swift 通過 where 描述這些需求:

1
2
3
4
5
6
7
8
9
10
11
func 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吸收了大量其它編程語言中的元素,這些元素包括但不限於:

  1. 屬性(Property)、可空值(Nullable type)語法和泛型(Generic Type)語法源自 C#。
  2. 格式風格與 Go 相仿(沒有句末的分號,判斷條件不需要括號)。
  3. Python風格的當前實例引用▓語法(使用 self)和列表字典聲明語法。
  4. Haskell風格的區間聲明語法(比如 1..31...3)。
  5. 協議和擴展源自 Objective-C(自家產品隨便用)。
  6. 枚舉類型很像 Java(可以擁有成員或方法)。
  7. classstruct 的概念和C#極其相似。

註意這裏不是說Swift是抄襲——實際上編程語言能玩的花樣基本就這些,況且Swift選的都是在我看來相當不錯的特性。

而且,這個大雜燴有一個好處——就是任何其它編程語言的開發者都不會覺得Swift很陌生——這一點很重要。

拒絕隱式(Refuse implicity)

Swift 去除了一些隱式操作,比如隱式類型轉換和隱式方法重載這兩個坑,幹的漂亮。

Swift的應用方向

我認為Swift主要有下面這兩個應用方向:

教育

我指的是編程教育。現有編程語言最大的問題就是交互性奇差,從而導致學習曲線陡峭。相信Swift及其交互性極強的編程環境能夠打破這個局面,讓更多的人——尤其是青少年,學會編程。

這裏有必要再次提到 Bret VictorInventing 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 best of C and Objective-C, without the constraints of C compatibility.</p> <p>Swift adopts safe programming patterns and adds modern features to make programming easier, more flexible and more fun.</p> <p>Swift’s clean slate, backed by the mature and much-loved Cocoa and Cocoa Touch frameworks, is an opportunity to imagine how software development 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>
我的算法學習之路 /blog/on-learning-algorithms/ 2014-05-04T20:43:42.000Z 2019-10-21T05:00:04.747Z 關於

嚴格來說,本文題目應該是 我的數據結構和算法學習之路,但這個寫法實在太繞口——況且CS中的算法往往暗指數據結構和算法(例如 算法導論 指的實際上是 數據結構和算法導論),所以我認為本文題目是合理的。

這篇文章講了什麽?

  • 我這些年學習數據結構和算法的總結。
  • 一些不錯的算法書籍和教程。
  • 算法的重要性。

初學

第一次接觸數據結構是在大二下學期的數據結構課程。然而這門課程並沒有讓我入門——當時自己正忙於倒▓賣各種MP3和耳機,對於這些課程根本就不屑一顧——反正最後考試劃個重點也能過,於是這門整個計算機專業本科最重要的課程就被傻逼的我直接忽略過去了。

直到大三我才反應過來以後還要找工作——而且大二的折騰證明了我並沒有什麽商業才能,以後還是得靠碼代碼混飯吃,我當時驚恐的發現自己對編程序幾乎一無所知,於是我給自己制訂了一個類似於建國初期五年計劃的讀書成長計劃,其中包括C語言基礎、數據結構以及計算機網絡等方面的書籍。

讀書計劃的第一步是選擇書籍,我曾向當時我覺得很牛的 “學長” 和 “大神” 請教應該讀哪些算法書籍,”學長”們均推薦算法導論,還有幾個”大神”推薦計算機程序設計藝術(現在我疑心他們是否翻過這些書),草草的翻了下這兩本書發現實在看不懂,但幸運的是我在無意中發現了 豆瓣 這個神奇的網站,裏面有很多質量不錯的書評,於是我就把評價很高而且看上去不那麽嚇人的計算機書籍都買了下來——事實證明豆瓣要比這些”學長”或是”大神”靠譜的多得多。

數據結構與算法分析——C 語言描述

數據結構與算法分析——C 語言描述

數據結構與算法分析——C 語言描述 是我學習數據結構的第一本書:當時有很多地方看不懂,於是做記號反復看;代碼看不明白,於是抄到本子上反復研讀;一些算法想不通,就把它所有的中間狀態全畫出來然後反復推演。事實證明盡管這種學習方法看起來傻逼而且效率很低,但對於當時同樣傻逼的我卻效果不錯——傻人用傻辦法嘛,而且這本書的課後題大多都是經典的面試題目,以至於日後我看到 編程之美 的第一反應就是這貨的題目不全是抄別人的麽。

至今記得,這本書為了說明算法是多麽重要,在開篇就拿最大子序列和作為例子,一路把復雜度從 O(N^3) 殺到 O(N^2) 再到 O(NlgN) 最後到 O(N),當時內心真的是景仰之情如滔滔江水連綿不絕,尼瑪為何可以這麽屌,

此外,我當時還把這本書裏圖算法之前的數據結構全手打了一遍,後來找實習還頗為自得的把這件事放到簡歷裏,現在▓想想真是傻逼無極限。

憑借這個讀書成長計劃中學到的知識,我總算比較順利的找到了一份實習工作,這是後話。

入門

我的實習並沒有用到什麽算法(現在看來就是不停的堆砌已有的 API,編寫一堆自己都不知道對不對的代碼而已),在發現身邊的人工作了幾年卻還在和我做同樣的事情之後,我開始越來越不安。盡管當時我對自己沒什麽規劃,但我清楚這絕壁不是我想做的工作。

微軟的夢工廠

微軟的夢工廠

在這個搖擺不定的時刻,微軟的夢工場 成了壓倒駱駝的最後一支稻草,這本書對微軟亞洲研究院的描寫讓我下定了 “找工作就要這樣的公司” 的決心,然而我又悲觀的發現無論是以我當時的能力還是文憑,都無法達到微軟亞研院的要求,矛盾之下,我徹底推翻了自己”畢業就工作”的想法,辭掉實習,準備考研。

考研的細節無需贅述,但至今仍清楚的記得自己在復試時驚奇且激動的發現北航宿舍對面就是微軟西格瑪大廈,那種離理想又進了一步的感覺簡直爽到爆。

算法設計與分析

我的研究生生涯絕對是一個反面典型——翹課,實習,寫水論文,做水研究,但有一點我頗為自得——從頭到尾認真聽了韓軍教授的算法設計與分析課程。

韓軍給我印象最深的有兩點:課堂休息時跑到外面和幾個學生借火抽煙;講解算法時的犀利和毫不含糊。

算法設計與分析基礎

盡管韓軍從來沒有主動提及,但我敢肯定 算法設計與分析基礎 就是他算法課程事實上的(de-facto)教材,因為他的課程結構幾乎和這本書的組織結構一模一樣。

如果 數據結構與算法分析——C語言描述 是我的數據結構啟蒙,那麽韓軍的課程 算法設計與分析基礎 就是我的算法啟蒙,結合課程和書籍,我一一理解並掌握了復雜度分析、分治、減治、變治、動態規劃和回溯這些簡單但強大的算法工具。

算法引論

算法引論

算法引論 是我這時無意中讀到的另一本算法書,和普通的算法書不同,這本書從創造性的角度出發——如果說算法導論講的是有哪些算法,那麽算法引論講的就是如何創造算法。結合前面的 算法設計與分析基礎,這本書把我能解決的算法問題數量擴大了一個數量級。

之後,在機緣巧合下,我進入微軟亞洲工程院實習,離理想又近了一步,自我感覺無限牛逼。

鞏固

在微軟工程院的實習是我研究生階段的一個非常非常非常重要的轉折點:

  1. 做出了一個還說的過去的小項目。
  2. 期間百度實習面試受挫,痛定思痛之下閱讀了大量的程序設計書。
  3. 微軟的實習經歷成為了我之後簡歷上為數不多的亮點之一(本屌一沒成績,二沒論文,三沒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 Programming

由於一直覺得名字裏帶 “Elements of” 的都是酷炫叼炸天的書,所以我幾乎是毫不猶豫的買了這本 Elements of Programming(中譯本:編程原本),事實上這本書裏的代碼(或者說 STL 的代碼)確實是:快,狠,準,古龍高手三要素全齊。

C Interfaces and Implementation

C Interfaces and Implementation

百度面試被爆出翔的經歷讓我意識到另一個問題,絕大多數公司面試時都需要在紙上寫 C 代碼,而我自己卻很少用 C(多數情況用 C#),考慮到自己還沒牛逼到能讓公司改變面試流程的地步,我需要提升自己編寫 C 代碼的能力(哪怕只是為了面試)。一頓 Google 之後,我鎖定了 C Interfaces and Implementation——另一本關於如何寫出狂炫酷帥叼炸天的C代碼的奇書,這裏套用下 Amazon 的 評論:Probably the best advanced C book 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 的情況下,只能自己動手搞一個:

Language Implementation Patterns

比較奇詭的是,做這個項目時當時我剛好把 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坑爹+漢字坑爹之余,還是得自己擼一個庫出來:

  1. 首先把那兩萬個漢字搞了出來,排序,然後弄成一個超長的字符串。
  2. 接下來用 Int16 索引了漢字所有的拼音(貌似500多個)。
  3. 再接下來用 Int64 建立漢字和拼音的關聯——漢字有多音字,所以需要把多個拼音 pack 到一個 Int64 裏,這個簡單,位操作就搞定。
  4. 最後用二分+位移 Unpack,直接█做到從漢字到拼音的檢索。
  5. 後來小測了下性能,速度是 MS 原來▓那個庫的五十倍有余,而代碼量只有 336 行。

用戶很 happy——因為我捎帶把他沒想到的多音字都搞定了,而且流暢的一逼。

我也很 happy,因為沒想到自己寫的庫居然比 MS 的還要快幾十倍,同時小十幾倍。

從這個事情之後我變得特別理解那些造輪子的人——你要想想,如果你需要一個飛機輪子但市場上只有自行車輪子而且老板還催著你交工,你能怎麽搞

快速字符串匹配

前面提到在微軟實習時老大扔給我一個 Windows Phone 讓我研究下,我當時玩了玩就覺著不太對勁,找聯系人太麻煩。

比如說找”張曉明”,WP 只支持定位到Z分類下——這意味著我需要在 Z 分類下的七十多個聯系人(姓張的姓趙的姓鐘的等等)裏面線性尋找,每次我都需要滑動四五秒才能找到這個張姓少年。

E51

這也太傻逼了,本屌三年前的老破NOKIA都支持首字母定位,996->ZXM->張曉明,直接搞定,尼瑪一個新時代 Windows Phone 居然會弱到這個程度。

搜了一下發現沒有好用的撥號程序,於是本屌就直接擼了一個支持首字母匹配的撥號程序出來扔到WP論壇裏。

結果馬上就有各種問題出現——最主要的反映是速度太慢,一些用戶甚至反饋按鍵有時要半秒才有反應。本屌問了下他的通訊錄大小:大概 3000 多人。

Cry

吐槽怎麽會有這麽奇葩的通訊錄之余,我意識到自己的字符串匹配算法存在嚴重的性能問題:讀取所有人的姓名計算出拼音,然後一個個的匹配——結果如果聯系人數量太多的話,速度必然拙計。

於是我就開始苦思冥想有沒有一個能夠同時搜索多個字符串的高端算法,以至於那兩天坐地鐵都在嘟囔怎麽才能把這個應用搞的快一些。

Algorithms on Strings, Trees and Sequences

最終還是在 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

說到優化這裏順帶提一下 Writing Efficient Programs——很難找到一本講代碼優化的書(我疑心是自從 Knuth 說了 過早優化是萬惡之源 之後沒人敢寫,萬惡之源嘛,寫它幹毛),註意這本書講的是代碼優化——在不改變架構、算法以及硬件的前提之下進行的優化。盡管書中的一些諸如變量復用或是循環展開的 trick 已經過時,但總體仍不失為一本好書。

提高

實習實習著就到了研二暑假,接下來就是求職季。

求職季時我有一種莫名的復仇感——尼瑪之前百度實習面試老子被你們黑的漫天飛翔,這回求職老子要把你們一個個黑回來,尼瑪。

現在回想當時的心理實屬傻逼+幼稚,但這種黑暗心理也起了一定的積極作用:我絲毫不敢有任何怠慢,以至於在5月份底我就開始準備求職筆試面試,比身邊的同學早了兩個月不止。

我沒有像身邊的同學那般刷題——而是繼續看書抄代碼學算法,因為我認為那些難得離譜的題面試官也不會問——事實上也是如此。

Algorithm Design Manual

Algorithm Design Manual

因為很多 Coding Interview 的論壇都提到這本 紅皮書,我也跟風搞了一本。事實證明,僅僅是關於 Backtrack Template 那部分的描述就足以值回書價,更不用說它的 Heuristics 和課後題。

編程珠璣&更多的編程珠璣

編程珠璣

更多的編程珠璣

這兩本書就不用多介紹,編程珠璣更多的編程珠璣,沒聽說過這兩本書請自行面壁。前者偏算法理論,後者偏算法軼事,前者提升能力,後者增長談資,都值得一讀。

The Science of Programming

The Science of Programming

讀到 編程珠璣 裏面關於 Binary Search 的正確性證明時我大呼過癮,原來程序的正確性也是可以推導的,然後我就在那一章的引用裏發現 David Gries 的 The Science of Programming。看名字就覺得很厲害,直接搞了一本開擼。

不愧為 編程珠璣 引用的書籍,讀完 The Science of Programming 之後,我獲得了證明簡單代碼段的正確性 這個技能——求職面試三板斧之二。

證明簡單代碼段的正確性 是一個很神奇的技能——因為面試時大多數公司都會要求在紙上寫一段代碼,然█後面試官檢查這段代碼,如果你能夠自己證明自己寫的代碼是正確的,面試官還能挑剔什麽呢?

之後就是各種面試,總之就是項目經歷紙上代碼正確性證明這三板斧,摧枯拉朽。

進化

求職畢業季之後就是各種 Happy,Happy 過後我發現即將面臨另一個問題:算法能力不足。

因為據說以後的同事大多是 ACM 選手,而本屌從來沒搞過算法競賽,而且知道的算法和數據結構都極為基礎:像那些元胞自動機、斐波那契堆或是線段樹這些高端數據結構壓根只是能把它們的英文名稱 拼寫 出來,連用都沒用過,所以心理忐忑的一逼。

為了不至於到時入職被鄙視的太慘烈,加上自己一貫的算法自卑癥,本屌強制自己再次學習算法:

算法(第四版)

算法(第四版)

算法(第四版) 是我重溫算法的第一本書,盡管它實際就是一本 數據結構的入門書,但它確實適合當時已經快把算法忘光的本屌——不為學習,只為重溫。

這本書最大的亮點在於它把 Visualization 和 Formatting 做到了極致——也許它不是最好的數據結構入門書,但它絕壁是我讀過的排版最好的書,閱讀體驗爽的一逼;當然這本書的內容也不錯,尤其是紅黑樹那一部分,我想不會有什麽書會比此書講的更明白。

6.851 Advanced Data Structures

Advanced Data Structures

Advanced Data Structures 是 MIT 的高級數據結構教程,為什麽會找到這個教程呢?因為Google Advanced Data 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 about automobiles, it would be for the person who wants to know how cars work, how they are built, and how one might design fuel-efficient, safe, reliable vehicles for the 21st century. The people who hate algorithms are the ones who just want to know how to drive their car on the highway, just like everyone else.

MIT 教授 Erik Demaine 則更為直接:

If you want to become 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>
學習 & 使用技術的四種層次 /blog/levels-on-learning-and-using-technologies/ 2014-04-13T00:53:21.000Z 2019-10-21T05:00:04.739Z 關於

Bjarne Stroustrup 在他的新書 A tour of C++

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 background stories, and usually given some suggestions what to see next…

…you do not know the city after 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

Salesman 是具有明確目的的 Tourist——他們學習技術的目標是為了完成某一項業務,就像旅行商去某地出差是為了賣商品而非觀光一樣。

絕大多數職業開發者在開發生涯中都會扮演 Salesman 這個角色——接到一個任務,涉及到某項不熟悉的技術,需要在限定時間內完成。

1.2. Sightseer(觀光者)

Sightseer

和 Salesman 相反,Sightseer 學習技術的目標是為了拓展視野,增加見識,而非完成某項特定業務。

具有主動學習精神的開發者在業余時會時常扮演 Sightseer 角色——找到自己認為有價值的新技術或是基礎知識進行系統學習,從而拓寬視野,提高水平。

2. Resident(居住者)

如果一個旅行者在一個地方待了半年以上,那麽他/她就會變得原來越像當地人。隨著 Tourist 對某項技術的日益精進,他/她會逐漸演變成這項技術的 Resident:

  • 熟悉這項技術的基本元素。
  • 熟悉這項技術的生態系統(Ecology):既包括開發工具(編輯器,命令行工具,集成開發環境等),也包括開發社區(討論組,郵件列表等)。
  • 了解這項技術能做什麽,不能做什麽。
  • 了解這項技術有那些坑,如何繞過這些坑,以及識別這些坑帶來的問題。
  • 對某些領域有深入的研究——但並不受限於特定領域。
  • 使用這項技術做出了有相當價值的產品或工具。

同 Tourist 一樣,根據使用技術的目標不同,Resident 可以分為 Worker 和 Craftsman:

2.1. Worker(工人)

Worker

技術是 Worker 的謀生手段,一個優秀的 Worker 應具備以下特征:

  • 對於給定問題,知道如何給出經濟有效的解決方案。
  • 以團隊合作為主,了解團隊合作的價值,能夠推動團隊項目健康前進。
  • 追求按時交付▓。

2.2. Craftsman(工匠)

Craftsman

同 Worker 不同,技術並非 Craftsman 的謀生手段,而是某種“副業”——用來提升聲望,修煉開發水平。

一個優秀的 Craftman 往往具備以下特點:

  • 對於給定問題,知道如何給出優雅的解決方案。
  • 以單兵作戰為主,主要靠個人推進項目,但也能進行一定程度的團隊合作。
  • 追求極致美感。

3. Architect(架構者)

有想法且有能力的人在一個地方待久了都會有將這個地方變的更好的沖動——一種方式是從源頭出發,推翻舊制度建立新社會,也就是革命;另一種方式則是保留現有的制度,對其進行溫和但持續的改進,也就是改良。

技術也是如此,任何技術都跟不上開發者成長的腳步,當這種差距到達一定程度時,就會有卓越的開發者站出來,創造出新的技術,他們就是 Architect:

  • 熟悉多項互相關聯的技術,並了解他們的優勢和不足。
  • 具備強大的領導能力,深厚的基礎和大量實際開發經驗。
  • 能夠帶動整個技術的生態系統發展。
  • 好吧,我編不下去了(尼瑪我要都知道我還至於是 IT 苦屌麽 -_-)

如果你看過Matrix 2: Reloaded

Matrix 2: Reloaded

就會知道 Architect 這個詞放在這裏再好不過。

根據目標不同,Architect 分為 Reformist 和 Revolutionist。

3.1. Reformist(改良者)

Reformist

改良者的目標:把現有技術變的更好。(Makes existing technology better)

例如:

3.2. Revolutionist(革命者)

Revolutionist

革命者的目標:用更好的技術取代現有技術。(Replaces existing technology with better 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),然後給出了各個頭銜所應具備的特征和能力。

關於同類文章

之前也有類似的文章,例如 程序員的十層境界開發者的八種境界▓

這些文章的共同點:

  1. 看似很牛逼但回想一下啥都沒說。
  2. 不會給人帶來什麽價值。
  3. 沒有一個鑒別的標準。
  4. 沒有指導性,也沒有使用價值。

本文的應用場景

考察狀態

以我自己對編程語言的掌握為例:

  • C/C++: Stranger.
  • Python: Craftsman.
  • Java: Worker.
  • C#: Craftsman.
  • JavaScript: Sightseer.
  • Scheme: Sightseer

將上面的列表轉置▓:

  • Stranger: C/C++
  • Sightseer: JavaScript, Scheme
  • Worker: Java
  • Craftsman: C#, Python

結合這些頭銜的定義,一目了然。

制定計劃

運用本文的詞匯,可以進行非常精煉的計劃制定:

  • 例如 Make a thoroughly sightseeing of C++
  • 或是 Become a 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 background stories, and usually given some suggestions what to see next…</p> <p>…you do not know the city after 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 分鐘實現一門編程語言——極簡解釋器教程 /blog/how-to-implement-an-interpreter-in-csharp/ 2014-03-23T19:08:35.000Z 2019-10-21T05:00:04.747Z 關於

本文介紹了如何使用 C# 實現一個簡化 Scheme——iScheme 及其解釋器。

如果你對下面的內容感興趣:

  • 實現基本的詞法分析,語法分析並生成抽象語法樹。
  • 實現嵌套作用域和函數調用。
  • 解釋器的基本原理。
  • 以及一些 C# 編程技巧。

那麽請繼續▓閱讀。

如果你對以下內容感興趣:

  • 高級的詞法/語法▓分析技術。
  • 類型推導/分析。
  • 目標代碼優化。

本文▓則過於初級,你可以跳過本文,但歡迎指出本文的錯誤 :-)

代碼樣例

代碼示例
1
2
3
4
5
6
7
8
9
public 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),解釋器並不會生成目標機器代碼,而是直█接運行源程序,簡單來說:

解釋器是運行程序的程序。

計算器就是一個典型的解釋器,我們把數學公式(源程序)給它,它通過運行它內部的”解釋器”給我們答案。

CASIO 計算器

iScheme 編程語言

iScheme 是什麽?

  • Scheme 語言的一個極簡子集。
  • 雖然小,但變量,算術|比較|邏輯運算,列表,函數和遞歸這些編程語言元素一應俱全。
  • 非常非常慢——可以說它只是為演示本文的概念而存在。

OK,那麽 Scheme 是什麽?

計算機程序的構造與解釋

以計算階乘為例:

C#版階乘
1
2
3
4
5
6
7
public static int Factorial(int n) {
if (n == 1) {
return 1;
} else {
return n * Factorial(n - 1);
}
}
iScheme版階乘
1
2
3
4
(def factorial (lambda (n) (
if (= n 1)
1
(* n (factorial (- n 1))))))

數值類型

由於 iScheme 只是一個用於演示的語言,所以目前只提供對整數的支持。iScheme 使用 C# 的 Int64 類型作為其內部的數值表示方法。

定義變量

iScheme使用`def`關鍵字定義變量
1
2
3
4
5
>> (def a 3)
>> 3
>> a
>> 3

算術|邏輯|比較操作

與常見的編程語言(C#, Java, C++, C)不同,Scheme 使用 波蘭表達式,即前綴表示法。例如:

C#中的算術|邏輯|比較操作
1
2
3
4
5
6
7
8
// Arithmetic ops
a + b * c
a / (b + c + d)
// Logical ops
(cond1 && cond2) || cond3
// Comparing ops
a == b
1 < a && a < 3
對應的iScheme代碼
1
2
3
4
5
6
7
8
; Arithmetic ops
(+ a (* b c))
(/ a (+ b c d))
; Logical ops
(or (and cond1 cond2) cond3)
; Comparing ops
(= a b)
(< 1 a 3)

需要註意的幾點:

  1. iScheme 中的操作符可以接受不止兩個參數——這在一定程度上控制了括號的數量。
  2. iScheme 邏輯操作使用 and , ornot 代替了常見的 && , ||! ——這在一定程度上增強了程序的可讀性。

順序語句

iScheme使用 begin 關鍵字標識順序語句,並以最後一條語句的值作為返回結果。以求兩個數的平均值為例:

C#的順序語句
1
2
3
int a = 3;
int b = 5;
int c = (a + b) / 2;
iScheme的順序語句
1
2
3
4
(def c (begin
(def a 3)
(def b 5)
(/ (+ a b) 2)))

控制流操作

iScheme 中的控制流操作只包含 if

if語句示例
1
2
3
4
5
>> (define a (if (> 3 2) 1 2))
>> 1
>> a
>> 1

列表類型

iScheme 使用 list 關鍵字定義列表,並提供 first 關鍵字獲取列表的第一個元素;提供 rest 關鍵字獲取列表除第一個元素外的元素。

iScheme的列表示例
1
2
3
4
5
6
7
8
>> (define alist (list 1 2 3 4))
>> (list 1 2 3 4)
>> (first alist)
>> 1
>> (rest alist)
>> (2 3 4)

定義函數

iScheme 使用 func 關鍵字定義函數:

iScheme的函數定義
1
2
3
(def square (func (x) (* x x)))
(def sum_square (func (a b) (+ (square a) (square b))))
對應的C#代碼
1
2
3
4
5
6
7
public static int Square (int x) {
return x * x;
}
public static int SumSquare(int a, int b) {
return Square(a) + Square(b);
}

遞歸

由於 iScheme 中沒有 forwhile 這種命令式語言(Imperative Programming Language)的循環結構,遞歸成了重復操作的唯一選擇。

以計算最大公約數為例:

iScheme計算最大公約數
1
2
3
4
(def gcd (func (a b)
(if (= b 0)
a
(func (b (% a b))))))
對應的C#代碼
1
2
3
4
5
6
7
public static int GCD (int a, int b) {
if (b == 0) {
return a;
} else {
return GCD(b, a % b);
}
}

高階函數

和 Scheme 一樣,函數在 iScheme 中是頭等對象▓,這意味著:

  • 可以定義一個變量為函數。
  • 函數可以接受一個函數作為參數。
  • 函數返回一個函數。
iScheme 的高階函數示例
1
2
3
4
5
6
7
8
9
10
11
; 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的詞法分析及測試
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public 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 類語言的程序即是語法樹,所以語法分析可以直接跳過。

以下面的程序為例:

程序即語法樹█
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;
(def x (if (> a 1) a 1))
; 換一個角度看的話:
(
def
x
(
if
(
>
a
1
)
a
1
)
)

更加直觀的圖片:

抽象語法樹

這使得抽象語法樹(Abstract Syntax Tree)的構建變得極其簡█單(無需考慮操作符優先級等問題),我們使用 SExpression 類型定義 iScheme 的語法樹(事實上S Expression也是Lisp表達式的名字)。

抽象語法樹的定義
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class 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;
}
}
}

然後用下面的步驟構建語法樹:

  1. 碰到左括號,創建一個新的節點到當前節點( current ),然後重設當前節點。
  2. 碰到右括號,回退到當前節點的父節點。
  3. 否則把為當前詞素創建節點,添加到當前節點中。
抽象語法樹的構建過程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public 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().ParseAsISchemeParseAsIScheme(Tokenize(code)) 流暢。
  • 大多數編程語言的語法分析不會這麽簡單!如果打算實現一個類似C#的編程語言,你需要更強大的語法分析技術:
    • 如果打算手寫語法分析器,可以參考 LL(k), Precedence Climbing 和Top Down Operator Precedence。
    • 如果打算生成語法分析器,可以參考 ANTLR 或 Bison。

作用域

作用域決定程序的運行環境。iScheme使用嵌套作用域。

以下面的程序為例

1
2
3
4
5
6
7
8
>> (def x 1)
>> 1
>> (def y (begin (def x 2) (* x x)))
>> 4
>> x
>> 1

作用域示例

利用C#提供的 Dictionary<TKey, TValue> 類型,我們可以很容易的實現 iScheme 的作用域 SScope

iScheme的作用域實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class 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

1
public class SObject { }

數值類型

iScheme 的數值類型只是對 .Net 中 Int64 (即 C# 裏的 long )的簡單封裝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class 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# 的隱式操作符重█載,這使得我們可以:

1
2
3
SNumber foo = 30;
SNumber bar = 40;
SNumber foobar = foo * bar;

而不必:

1
2
3
SNumber foo = new SNumber(value: 30);
SNumber bar = new SNumber(value: 40);
SNumber foobar = new SNumber(value: foo.Value * bar.Value);

為了方便,這裏也為 SObject 增加了隱式操作符重載(盡管 Int64 可以被轉換為 SNumberSNumber 繼承自 SObject ,但 .Net 無法直接把 Int64 轉化為 SObject ):

1
2
3
4
5
6
public class SObject {
...
public static implicit operator SObject(Int64 value) {
return (SNumber)value;
}
}

Bool類型

由於 Bool 類型只有 True 和 False,所以使用靜態對象就足矣。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class 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# 的 隱式操作符重載,這使得我們可以:

1
2
3
4
SBool foo = a > 1;
if (foo) {
// Do something...
}

而不用

1
2
3
4
SBool foo = a > 1 ? SBool.True: SBool.False;
if (foo == SBool.True) {
// Do something...
}

同樣,為 SObject 增加 隱式操作符重載

1
2
3
4
5
6
public class SObject {
...
public static implicit operator SObject(Boolean value) {
return (SBool)value;
}
}

列表類型

iScheme使用.Net中的 IEnumberable<T> 實現列表類型 SList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class 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的實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SFunction : SObject {
public SExpression Body { get; private set; }
public String[] Parameters { get; private set; }
public SScope Scope { 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),這意味著:
部分求值
1
2
3
4
5
6
7
8
9
10
11
>> (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)的數量時,它依然是一個函數,無法被求值。

這個功能有什麽用呢?生成高階函數。有了部分求值,我們就可以▓使用

1
2
3
4
5
(def mul (func (a b) (* a b)))
(def mul3 (mul 3))
>> (mul3 3)
>> 9

而不用專門定義一個生成函數:

1
2
3
4
5
(def times (func (n) (func (n x) (* n x)) ) )
(def mul3 (times 3))
>> (mul3 3)
>> 9
  • SFunction#ToString 可以將其自身還原為源代碼——從而大大簡化了 iScheme 的理解和測試。

內置操作

iScheme 的內置操作有四種:算術|邏輯|比較|列表操作。

我選擇了表達力(Expressiveness)強的 lambda 方法表來定義內置操作:

首先在 SScope 中添加靜態字段 builtinFunctions ,以及對應的訪問屬性 BuiltinFunctions 和操作方法 BuildIn

1
2
3
4
5
6
7
8
9
10
11
12
public class 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;
}
}

註意:

  1. Func<T1, T2, TRESULT> 是 C# 提供的委托類型,表示一個接受 T1T2 ,返回 TRESULT
  2. 這裏有一個小 HACK,使用實例方法(Instance Method)修改靜態成員(Static Member),從而實現一套流暢的 API(參見Fluent Interface)。

接下來就可以這樣定義內置操作:

1
2
3
4
5
new SScope(parent: null)
.BuildIn("+", addMethod)
.BuildIn("-", subMethod)
.BuildIn("*", mulMethod)
.BuildIn("/", divMethod);

一目了然。

斷言(Assertion)擴展

為了便於進行斷言,我對 Boolean 類型做了一點點擴展。

1
2
3
public 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.");

而不用

1
2
3
if (a < 3) {
throw new Exception("Value must be less than 3.");
}

算術操作

iScheme 算術操作包含 + - * / % 五個操作,它們僅應用於數值類型(也就是 SNumber )。

從加減法開始:

1
2
3
4
5
6
7
8
9
10
11
12
.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),考慮到接下來還有不少地方要用這個邏輯,我把這段邏輯抽象成▓擴展方法:

1
2
3
4
5
6
7
public 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));
}

然後加減法就可以如此定義:

1
2
3
4
5
6
7
8
9
.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);
})

乘法,除法和求模定義如下:

1
2
3
4
5
6
7
8
9
10
11
.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 邏輯操作包括 andornot ,即與,或和非。

需要註意的是 iScheme 邏輯操作是 短路求值(Short-circuit evaluation),也就是說:

  • (and condA condB) ,如果 condA 為假,那麽整個表達式為假,無需對 condB 求值。
  • (or condA condB) ,如果 condA 為真,那麽整個表達式為真,無需對 condB 求值。

此外和 + - * / 一樣, andor 也可以接收任意數量的參數。

需求明確了接下來就是實現,iScheme 的邏輯操作實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
.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 的比較操作包括 = < > >= <= ,需要註意下面幾點:

  • = 是比較操作而非賦值操作。
  • 同算術操作一樣,它們應用於數值類型,並支持任意數量的參數。

    = 的實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
.BuildIn("=", (args, scope) => {
(args.Length > 1).OrThrows("Must have more than 1 argument 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;
})

可以預見所有的比較操作都將使用這段邏輯,因此把這段比較邏輯抽象成一個擴展方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static SBool ChainRelation(this SExpression[] expressions, SScope scope, Func<SNumber, SNumber, Boolean> relation) {
(expressions.Length > 1).OrThrows("Must have more 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;
}

接下來就可以很方便的定義比較操作:

1
2
3
4
5
.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 的列表操作包括 firstrestempty?append ,分別用來取列表的第一個元素,除第一個以外的部分,判斷列表是否為空和拼接列表。

firstrest 操作如下:

1
2
3
4
5
6
7
8
9
10
.BuildIn("first", (args, scope) => {
SList list = null;
(args.Length == 1 && (list = (args[0].Evaluate(scope) as SList)) != null).OrThrows("<first> must apply 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 apply to a list.");
return new SList(list.Skip(1));
})

又發現相當的重復邏輯——判斷參數是否是一個合法的列表,重復代碼很邪惡,所以這裏把這段邏輯抽象為擴展方法:

1
2
3
4
5
6
public 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 apply to a list");
return list;
}

有了這個擴展方法,接下來的列表操作就很容易實現:

1
2
3
4
5
6
7
8
9
10
.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)循環:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public 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));
}
} catch (Exception ex) {
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(">> " + ex.Message);
}
}
}

然後在 SExpression#Evaluate 中補充調用代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public 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 中調用該解釋/求值循環:

1
2
3
4
5
6
7
static 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 裏面:

1
2
3
4
5
6
public class SExpression {
// ...
public override SObject Evaluate(SScope scope) {
// TODO: Todo your ass.
}
}

首先明確輸入和輸出:

  1. 處理字面量(Literals): 3 ;和具名量(Named Values): x
  2. 處理 if(if (< a 3) 3 a)
  3. 處理 def(def pi 3.14)
  4. 處理 begin(begin (def a 3) (* a a))
  5. 處理 func(func (x) (* x x))
  6. 處理內置函數調用:(+ 1 2 3 (first (list 1 2)))
  7. 處理自定義函數調用:(map (func (x) (* x x)) (list 1 2 3))

此外,情況1和2中的 SExpression 沒有子節點,可以直接讀取其 Value 進行求值,余下的情況需█要讀取其 Children 進行求值。

首先處理沒有子節點的情況:

處理字面量和具名量

1
2
3
4
5
6
7
8
if (this.Children.Count == 0) {
Int64 number;
if (Int64.TryParse(this.Value, out number)) {
return number;
} else {
return scope.Find(this.Value);
}
}

接下來處理帶有子節點的情況:

首先獲得當前節點的第一個節點:

1
SExpression first = this.Children[0];

然後根據該節點的 Value 決定下一步操作:

處理 if

if 語句的處理方法很直接——根據判斷條件(condition)的值判斷執行哪條語句即可:

1
2
3
4
if (first.Value == "if") {
SBool condition = (SBool)(this.Children[1].Evaluate(scope));
return condition ? this.Children[2].Evaluate(scope) : this.Children[3].Evaluate(scope);
}

處理 def

直接定義即可:

1
2
3
else if (first.Value == "def") {
return scope.Define(this.Children[1].Value, this.Children[2].Evaluate(new SScope(scope)));
}

處理 begin

遍歷語句,然後返回最後一條語句的值:

1
2
3
4
5
6
7
else if (first.Value == "begin") {
SObject result = null;
foreach (SExpression statement in this.Children.Skip(1)) {
result = statement.Evaluate(scope);
}
return result;
}

處理 func

利用 SExpression 構建 SFunction ,然後返回:

1
2
3
4
5
6
else if (first.Value == "func") {
SExpression body = 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

1
2
3
else if (first.Value == "list") {
return new SList(this.Children.Skip(1).Select(exp => exp.Evaluate(scope)));
}

處理內置操作

首先得到參數的表達式,然後調用對應的內置函數:

1
2
3
4
else if (SScope.BuiltinFunctions.ContainsKey(first.Value)) {
var arguments = this.Children.Skip(1).ToArray();
return SScope.BuiltinFunctions[first.Value](arguments, scope);
}

處理自定義函數調用

自定義函數調用有兩種情況:

  1. 非具名函數調用:((func (x) (* x x)) 3)
  2. 具名函數調用:(square 3)

調用自定義函數時應使用新的作用域,所以為 SFunction 增加 Update 方法:

1
2
3
4
5
6
public 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 增加 SpawnScopeWithFindInTop 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SScope SpawnScopeWith(String[] names, SObject[] values) {
(names.Length >= values.Length).OrThrows("Too many arguments.");
SScope scope = 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;
}

下面是函數調用的實現:

1
2
3
4
5
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();
}

完整的求值代碼

綜上所述,求值代碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public 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 body = 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)轉化為叠代的方法,細節恕不贅述,以階乘為例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 call 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 ,得到轉換後的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public 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 body = 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;
scope = newFunction.Scope;
}
}
}
}
}

一些演示

基本的運算

基本的運算

高階函數

高階函數

回顧

小結

除去註釋(貌似沒有註釋-_-),iScheme 的解釋器的實現代碼一共 333 行——包括空行,括號等元素。

在這 300 余行代碼裏,實現了函數式編程語言的大部分功能:算術|邏輯|運算,嵌套作用域,順序語句,控制語句,遞歸,高階函數部分求值

與我兩年之前實現的 Scheme 方言 Lucida相比,iScheme 除了沒有字符串類型,其它功能和Lucida相同,而代碼量只是前者的八分之一,編寫時間是前者的十分之一(Lucida 用了兩天,iScheme 用了一個半小時),可擴展性和易讀性均秒殺前者。這說明了:

  1. 代碼量不能說明問題。
  2. 不同開發者生產效率的差別會非常巨大。
  3. 這兩年我還是學到了一點東西的。-_-

一些設計決策

使用擴展方法提高可讀性

例如,通過定義 OrThrows

1
2
3
public 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 函數為例:

1
2
3
4
5
6
7
static 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還是有很多不足:

語言特性方面:

  1. 缺乏實用類型:沒有 DoubleString 這兩個關鍵類型,更不用說復合類型(Compound Type)。
  2. 沒有IO操作,更不要說網絡通信。
  3. 效率低下:盡管去除尾遞歸挽回了一點效率,但iScheme的執行效率依然慘不忍睹。
  4. 錯誤信息:錯誤信息基本不可讀,往往出錯了都不知道從哪裏找起。
  5. 不支持延續調用(Call with current continuation,即call/cc)。
  6. 沒有並發。
  7. 各種bug:比如可以定義文本量,無法重載默認操作,空括號被識別等等。

設計實現方面:

  1. 使用了可變(Mutable)類型。
  2. 沒有任何註釋(因為覺得沒有必要 -_-)。
  3. 糟糕的類型系統:Lisp類語言中的數據和程序可以不分彼此,而iScheme的實現中確把數據和程序分成█了 SObjectSExpression ,現在我依然沒有找到一個融合他們的好辦法。

這些就留到以後慢慢處理了 -_-(TODO YOUR ASS)

延伸閱讀

書籍

  1. Compilers: Priciples, Techniques and Tools Principles:
  2. Language Implementation Patterns:
  3. *The Definitive ANTLR4 Reference:
  4. Engineering a compiler:
  5. Flex & Bison:
  6. *Writing Compilers and Interpreters:
  7. Elements of Programming:

註:帶*號的沒有中譯本。

文章

大多和編譯前端相關,自己沒時間也沒能力研究後端。-_-

為什麽編譯技術很重要?看看 Steve Yegge(沒錯,就是被王垠黑過的 Google 高級技術工程師)是怎麽說的(需要翻墻)。

http://steve-yegge.blogspot.co.uk/2007/06/rich-programmer-food.html

本文重點參考的 Peter Norvig 的兩篇文章:

  1. How to write a lisp interpreter in Python:
  2. An even better lisp interpreter in Python:

幾種簡單實用的語法分析技術:

  1. LL(k) Parsing:
  2. Top Down Operator Precendence:
  3. Precendence Climbing Parsing:
]]>
<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>
如何閱讀書籍 /blog/on-reading-books/ 2014-03-15T20:05:35.000Z 2019-10-21T05:00:04.747Z 摘要

這篇文章從如何閱讀書籍出發,簡單討論了如何選擇書籍、是否閱讀原版和閱讀數量這幾個常見問題,然後自己的閱讀問題進行了分析和總結。

註意

  1. “如何閱讀” 指 “What to read” 而非 “How to read”,Mortimer J. Adle r的 怎樣閱讀一本書 對How to read有著精彩█的描述。
  2. “書籍”指非小說(Non-fiction)類書籍。

目標

我是一個功利主義者(Utilitarianism),因此我認為閱讀的目標在於為自己創造實█際價值,所以:

  1. 我不會因為某本書看起來很有趣就去閱讀(機會成本)。
  2. 也不會因為很多人推薦某本書就去閱讀(從眾)。
  3. 更不會因為某本書難就去閱讀(追求智商優越感)

一本書值得閱讀,當且僅當:

  1. 它可以直接為我創造價值。
  2. 它可以間接為我創造價值。

我的閱讀目標:

形成T型知識結構:專業知識盡可能深入,專業周邊知識盡可能精煉。

如何選擇?

專業書籍

專業知識盡可能深入。

我是一個軟件開發者(Software Developer),因此這裏的專業書籍均和軟件開▓發有關。

這裏介紹我自己用的兩種方法:

根據引用列表

從一本經典書籍出發,深度優先遍歷它的引用列表,通過書評和摘要了解這些引用書籍,再根據自己的實際情況決定自己的閱讀次序。

這裏以 代碼大全 為例(為了方便和一致性,這裏使用英文書名):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Code 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本經典書籍要容易的多;
  • 質量保證:靠譜書籍的引用書籍的質量一般都很高;
  • 發現一些被忽視的經典:相當一部分的書籍隨著時間的流逝而淡出人們的視野,但這並不代表它們本身沒有價值,例如:
  • 形成知識體系:引用書籍彼此具有天然的聯系,這使得創建知識體系更加容易。

我認為這種方法適用於任何需要嚴肅閱讀的領域:

  1. 錨點:找到一本經典書籍。
  2. 撒網:了解該書引用列表中的書籍。
  3. 收網:根據自己實際需要,精讀相關書籍▓。

根據作者

這裏以計算機書籍為例(以下僅代表個人口味):

  1. Richard Stevens:善。
  2. Brian Kernighan:極善。
  3. Deitel Series:翔。
  4. Bruce Eckel:廢話連篇。
  5. Jon Bentley:善。
  6. Andrew S Tanenbaum:大善。
  7. Jeffrey D Ullman:善。
  8. P.J. Plauger:大善。
  9. Robert C Martin:善。
  10. Bjarne Stroustrup:善,但略神叨(神侃世界觀方法論有點頂不住)。
  11. Martin Fowler:善,但略嘮叨。
  12. Ron Jeffries:翔(好吧我是故意來黑的,尼瑪連個Sudoku都解不出來寫毛程序)

這種方法█的問題在於需要一定閱讀經驗,但是它非常有效——以至於不用看內容就對書的質量有七八成把握。

非本專業書籍

專業周邊知識盡可能精煉。

  1. 對於專業周邊知識,了解關鍵概念及指導思想即可。
  2. 不需要,也沒有必要對專業周邊知識進行深入了解。
  3. “Know what” is enough, “Know how” is expensive.

以我2年前編寫手機應用,學習用戶體驗為例:

  1. 分別在現實中(身邊有幾個很不錯的交互設計師)和線上(Quora和知乎)進行提問和搜索,得到一個書單。
  2. 按照下面的原則過濾書單:
    • 去掉教科書和大部頭。
    • 去掉包含大量原理或論證的書籍。
    • 保留結論型書籍。
    • 保留指南型書籍。
  3. 總結出書單,迅速的閱讀並找到關鍵點。

了解設計的人可能認為上面的書單過於初級——沒錯,它們都是結論型或指南型書籍,沒有原理,也沒有論證——但這正是對於我這樣的非專業者所需要的書籍:我不需要知道這些知識是怎麽來的,知道█怎麽用足矣。

此外,受價值█驅動,而非興趣——大多數情況下興趣只是把自己脫離當前困境的接口。

學習型書籍

學習型書籍是一種元(Meta)方法書籍:這類書籍用於提升學習能力,換句話說,就是縮短吸收知識所需要的時間。

這類書籍我只讀過下面的幾本,效果有但不明顯:

需要註意的是,不要陷入到尋求最優學習方法的誤區——Best is the worthest enemy of better。

閱讀原版?

如何在翻譯版和原版做選擇?

  1. 優先選擇翻譯版。計算機書籍這種描述精確知識的書籍更是█如此。
  2. 此外,如果閱讀中出現難以理解的問題,不要下意識的把其歸咎於翻譯問題——多數情況是理解問題。

為什麽還有那麽多人閱讀原版?

  1. 因為翻譯版還沒出版。
  2. 知識的價值有其時效性。
  3. 逼格。

越多越好?

我經常逛豆瓣,豆瓣有一個很有意思的現象就是人們喜歡去比較自己每年讀書的數量,或者是截圖炫耀自己讀過幾千本書雲雲。

我在█這裏酸一下:書的數量並沒有什麽參考價值,就好比無法用蓋一棟大樓的磚數評價這棟大樓的質量;換個說法,Effort 不等於 Progress。

關鍵在於讀過書的質量,吸收的程度,以及創造的價值。

此外,盲目追求讀書的數量會帶來另一個問題——淺嘗輒止。本應花在專業書籍上的時間被分配到其它無關緊要的事情上,導致該學好的沒學好,沒必要的學了一灘但用不上。

總結

  1. 形成 T 型知識█結構:專業知識盡可能深入,專業周邊知識盡可能精煉。
    • 按照引用列表和作者深入閱讀專業書籍。
    • 利用結論型/指南型書籍精煉閱讀專業周邊書籍。
    • 不斷強化自己的按需學習能力。
  2. 不一定非要閱讀原版。
  3. 讀書並非多多益善。
  4. 讀書之前回答下面幾個問題:
    • 這本書能給自己帶來什麽改變?
    • 自己是否需要這種改變?
    • 如果均為 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>