程式設計裡的“小聰明”(1)

發表於2013-03-18

來源:王垠的部落格

很早就想寫這樣一篇博文了,可是一直沒來得及動筆。在學校的時候,時間似乎總是不夠用,因為一旦有點時間,你就想是不是該用來多看點論文。所以我很高興,工作的生活給了我真正自由的時間,讓我可以多分享一些自己的經驗。

我今天想開始寫這系列文章的原因是,很多程式設計師的頭腦中都有一些通過“非理性”方式得到的錯誤觀點。這些觀點如此之深,以至於你沒法跟他們講清楚。即使講清楚了,一般來說也很難改變他們的習慣。

程式設計師的世界,是一個“以傲服人”的世界,而不是一個理性的,“以德服人”的世界。很多人喜歡在程式裡耍一些“小聰明”,以顯示自己的與眾不同。由於這些人的名氣和威望,人們對這些小聰明往往不加思索的吸收,以至於不知不覺學會了很多表面上聰明,其實導致不必要麻煩的思想,根深蒂固,難以去除。接著,他們又通過自己的“傲氣”,把這些錯誤的思想傳播給下一代的程式設計師,從而導致惡性迴圈。人們總是說“聰明反被聰明誤”,程式設計師的世界裡,為這樣的“小聰明”所栽的根頭,可真是數不勝數。以至於直到今天,我們仍然在疲於彌補前人所犯下的錯誤。

所以從今天開始,我打算陸續把自己對這些“小聰明”的看法記錄在這裡,希望看了的人能夠發現自己頭腦裡潛移默化的錯誤,真正提高程式碼的“境界”。可能一下子難以記錄所有這類誤區,不過就這樣先開個頭吧。

小聰明1:片面追求“短小”

我經常以自己寫“非常短小”的程式碼為豪。有一些人聽了之後很讚賞,然後說他也很喜歡寫短小的程式碼,接著就開始說 C 語言其實有很多巧妙的設計,可以讓程式碼變得非常短小。然後我才發現,這些人所謂的“短小”跟我所說的“短小”,完全不是一回事。

我的程式的“短小”,是建立在語義明確,概念清晰的基礎上的。在此基礎上,我力求去掉冗餘的,繞彎子的,混淆的程式碼,讓程式更加直接,更加高效的表達我心中設想的“模型”。這是一種在概念級別的優化,而程式的短小精悍只是它的一種“表象”。這種短小,往往是在“語義” (semantics) 層面的,而不是在“語法”層面死摳幾行程式碼。我絕不會為了程式“顯得短小”而讓它變得難以理解,或者容易出錯。

相反,很多其它人所追求的“短小”,卻是盲目的,沒有原則的。在很多時候,這些小伎倆都只是在“語法” (syntax) 層面,比如,想辦法把兩行程式碼寫成一行。可以說,這種“片面追求短小”的錯誤傾向,造就了一批語言設計上的錯誤,以及一批“擅長於”使用這些錯誤的程式設計師。

舉一個簡單的例子,就是很多語言裡都有的 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(static single assignment)。我只是通過這個簡單的例子讓你意識到,++操作不但帶來了程式的混淆,而且延緩甚至阻礙了人們發明像 SSA 這樣的技術。如果人們早一點從本質上意識到 i = i + 1 的含義(其實裡面的兩個 i 是完全不同的變數),那麼 SSA 可能會提前很多年被髮明出來。

(好了,到這裡我承認,想用一段話講清楚這個問題的企圖,失敗了。)

所以,有些人很在乎 i++ 與 ++i 的區別,去追究 (i++) + (++i) 這類表示式的含義,追究 i++ 與 ++i 誰的效率更高,…… 其實都是徒勞的。“精通”這些細微的問題,並不能讓你成為一個好的程式設計師。真正正確的做法其實是:完全不使用 i++ 或者 ++i。

當然由於人們約定俗成的習慣,在某種非常固定,非常簡單,眾人皆知的“模式”下,你還是可以使用 i++ 和 ++i。比如: for (int i=0; i < n; i++)。但是除此之外,最好不要在任何其它地方使用。特別不要把 ++ 放在表示式中間,或者函式的引數位置,比如 a[i++], f(++i) 等等,因為那樣程式就會變得難以理解,容易出錯。如果你把兩個以上的 ++ 放在同一個表示式裡,就會造成“非確定性”的錯誤。這種錯誤會造成程式可能在不同的編譯器下出現不同的結果。

雖然我對這些都瞭解的非常清楚,但我不想繼續探討這些問題。因為與其記住這些,不如完全忘記 i++ 和 ++i 的存在。

好了,一個小小的例子,也許已經讓你意識到了片面追求短小程式所帶來的巨大代價。很可惜的是,程式語言的設計者們仍然在繼續為此犯下類似的錯誤。一些“新”的語言,設計了很多類似的,旨在“縮短程式碼”,“減少打字量”的雕蟲小技。也許有一天你會發現,這些雕蟲小技所帶來的,除了短暫的興奮,剩下的就是無盡的煩惱。

思考題:

1. Google 公司的程式碼規範裡面規定,在任何情況下 for 語句和 if 語句之後必須寫花括號,即使 C 和 Java 允許你在其只包含一行程式碼的時候省略它們。比如,你不能這樣寫

而必須寫成

請分析:這樣多寫兩個花括號,是好還是不好?

2. 當我第二次到 Google 實習的時候,發現我一年前給他們寫的程式碼,很多被調整了結構。幾乎所有如下結構的程式碼:

都被人改成了:

請問這裡省略了一個“else”和兩個花括號,會帶來什麼好處或者壞處?

3. 根據本文對於 ++ 操作的看法,再參考傳統的圖靈機的設計,你是否發現圖靈機的設計存在類似的問題?你如何改造圖靈機,使得它不再存在這種問題?

4. 參考這個《Go 語言入門指南》,看看你是否能從中發現由於“片面追求短小”而產生的,別的語言裡都沒有的設計錯誤?

相關文章