王垠:程序設計里的“小聰明”
很早就想寫這樣一篇博文了,可是一直沒來得及動筆。在學校的時候,時間似乎總是不夠用,因為一旦有點時間,你就想是不是該用來多看點論文。所以我很高興,工作的生活給了我真正自由的時間,讓我可以多分享一些自己的經驗。
我今天想開始寫這系列文章的原因是,很多程序員的頭腦中都有一些通過“非理性”方式得到的錯誤觀點。這些觀點如此之深,以至于你沒法跟他們講清楚。即使講清楚了,一般來說也很難改變他們的習慣。
程序員的世界,是一個“以傲服人”的世界,而不是一個理性的,“以德服人”的世界。很多人喜歡在程序里耍一些“小聰明”,以顯示自己的與眾不同。由于這些人的名氣和威望,人們對這些小聰明往往不加思索的吸收,以至于不知不覺學會了很多表面上聰明,其實導致不必要麻煩的思想,根深蒂固,難以去除。接著,他們又通過自己的“傲氣”,把這些錯誤的思想傳播給下一代的程序員,從而導致惡性循環。人們總是說“聰明反被聰明誤”,程序員的世界里,為這樣的“小聰明”所栽的根頭,可真是數不勝數。以至于直到今天,我們仍然在疲于彌補前人所犯下的錯誤。
所以從今天開始,我打算陸續把自己對這些“小聰明”的看法記錄在這里,希望看了的人能夠發現自己頭腦里潛移默化的錯誤,真正提高代碼的“境界”。可能一下子難以記錄所有這類誤區,不過就這樣先開個頭吧。
小聰明1:片面追求“短小”
我經常以自己寫“非常短小”的代碼為豪。有一些人聽了之后很贊賞,然后說他也很喜歡寫短小的代碼,接著就開始說 C 語言其實有很多巧妙的設計,可以讓代碼變得非常短小。然后我才發現,這些人所謂的“短小”跟我所說的“短小”,完全不是一回事。
我的程序的“短小”,是建立在語義明確,概念清晰的基礎上的。在此基礎上,我力求去掉冗余的,繞彎子的,混淆的代碼,讓程序更加直接,更加高效的表達我心中設想的“模型”。這是一種在概念級別的優化,它其實只是間接的導致了程序的短小精悍。這種短小,往往是在“語義” (semantics) 層面的,而不只是在“語法”層面死摳幾行代碼。我絕不會為了程序“顯得短小”而讓它變得難以理解,或者容易出錯。
相反,很多其它人所追求的“短小”,卻是盲目的,沒有原則的小伎倆。在很多時候,這些小伎倆都只是在“語法” (syntax) 層面,比如,想辦法把兩行代碼寫成一行。可以說,這種“片面追求短小”的錯誤傾向,造就了一批語言設計上的錯誤,以及一批“擅長于”使用這些錯誤的程序員。
舉一個簡單的例子,就是很多語言里都有的 i++ 和 ++i 這兩個“自增”操作。很多人喜歡在代碼里使用這兩個東西,是因為這樣可以“節省一行代碼”。殊不知,節省掉的那區區幾行代碼,比起由于使用自增操作帶來的混淆和錯誤,其實是九牛之一毛。
從理論上講,i++ 和 ++i 本身就是錯誤的設計。因為它們把對變量的“讀”和“寫”兩種根本不同的操作,毫無原則的合并在一起。這種對讀寫操作的混淆不清,帶來了非常難以發現的錯誤,甚至在某些時候帶來效率的低下。
相反,等價的一種“笨”一點的寫法,i = i + 1,不但更易理解,而且更符合程序內在的一種精妙的“哲學”原理。這個原理,其實來自一句古老的諺語:你不能踏進同一條河流兩次。也就是說,當你第二次踏進“這條河”的時候,它已經不再是之前的那條河!這聽起來有點玄,但是我希望能夠用一段話解釋清楚它跟 i = i + 1 的關系:
現在來想象一下,你就是超人卡卡西,你擁有明察秋毫的“寫輪眼”,你能看到處理器的每一步微小的操作,每一個電子的流動。現在對你來說,i = i + 1 的含義是,讓 i 和 1 進入“加法器”。i 和 1 所含有的信息,以 bit 為大小,被加法器的線路分解,組合。經過這樣一番復雜的轉換之后,在加法器的“輸出端”,出現了一個“新”的整數,它的值比 i 要大 1。接著,這個新的整數通過電子線路,被放進“另一個”變量,這個變量的名字,“碰巧”也叫做 i。特別注意我加了引號的詞,你是否能用頭腦想象出電子線路里面信息的流動?
我是在告訴你,i = i + 1 里面的第一個 i 跟第二個 i,其實是兩個完全不同的變量——它們只不過名字相同而已!如果你把它們換個名字,就可以寫成 i2 = i1 + 1。當然,你需要把這條語句之后的所有的 i 全都換成 i2(直到 i 再次被“賦值”)。這樣變換之后,程序的語義不會發生改變。
我是在說廢話嗎?這樣把名字換來換去有什么意義呢?如果你了解編譯器的設計,就會發現,其實我剛剛告訴你的哲學思想,足以讓你“重新發明”出一種先進的編譯器技術,叫做 SSA(single static assignment)。我只是通過這個簡單的例子讓你意識到,i++ 和 ++i 不但帶來了程序的混淆,而且延緩甚至阻礙了人們發明像 SSA 這樣的技術。如果人們早一點從本質上意識到 i = i + 1 的含義(其實里面的兩個 i 是完全不同的變量),那么 SSA 很可能會提前很多年被發明出來。
(好了,到這里我承認,想用一段話講清楚這個問題的企圖,失敗了。)
所以,有些人很在乎 i++ 與 ++i 的區別,去追究 (i++) + (++i) 這類表達式的含義,其實是徒勞的。“精通”這些細微的問題,并不能讓你成為一個好的程序員。真正正確的做法其實是:完全不使用 i++ 或者 ++i。當然由于人們約定俗成的習慣,在某種非常固定,非常簡單的,眾人皆知“模式”下,你還是可以使用 i++ 和 ++i。比如: for (int i=0; i < n; i++)。但是除此之外,最好不要在任何其它地方使用。
如果你把它們放在表達式中間,或者函數的參數位置,比如 a[i++], f (++i) 等等,那么程序就會變得難以理解。而如果你把兩個以上的 i++ 放在同一個表達式里,就會造成“非確定性”的錯誤。這種錯誤會造成程序在不同的編譯器下出現不同的結果。
雖然我對這些都了解的非常清楚,但我不想繼續探討這些問題。因為與其記住這些,不如完全忘記 i++ 和 ++i 的存在。
好了,一個小小的例子,也許已經讓你意識到了片面追求短小程序所帶來的巨大代價。很可惜的是,程序語言的設計者們仍然在繼續為此犯下類似的錯誤。一些“新”的語言,設計了很多類似的,旨在“縮短代碼”,“減少打字量”的雕蟲小技。也許有一天你會發現,這些雕蟲小技所帶來的,在短暫的興奮之后,其實是無窮無盡的煩惱。
思考題:
1. Google 公司的“代碼規范”里面規定,在任何情況下 for 語句和 if 語句之后必須寫花括號,即使 C 和 Java 允許你在其只包含一行代碼的時候省略它們。比如,你不能這樣寫
- for (int i=0; i < n; i++)
- some_function (i);
而必須寫成
- for (int i=0; i < n; i++) {
- some_function (i);
- }
請分析:這樣的代碼規范,是好還是不好?請說明理由。
2. 當我第二次到 Google 實習的時候,發現我的一年前的代碼很多被調整了結構。幾乎所有如下結構的代碼:
- if (x > 0) {
- return 0;
- } else {
- return 1;
- }
都被人改成了:
- if (x > 0) {
- return 0;
- }
- return 1;
請問這里省略了一個“else”和兩個花括號,會帶來什么好處或者壞處?
3. 根據本文對于 ++ 操作的看法,再參考傳統的圖靈機的設計,你是否發現圖靈機的設計存在類似的問題?你如何改造圖靈機,使得它不再存在這種問題?
4. 參考這個《Go 語言入門指南》,看看你是否能從中發現由于“片面追求短小”而產生的,別的語言里都沒有的設計錯誤?