1. 程式人生 > >【轉】程序語言的常見設計錯誤(1) - 片面追求短小

【轉】程序語言的常見設計錯誤(1) - 片面追求短小

body 缺陷 優化 簡單 code 不知道 返回 技巧 兩種

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

我的程序的“短小”是建立在語義明確,概念清晰的基礎上的。在此基礎上,我力求去掉冗余的,繞彎子的,混淆的代碼,讓程序更加直接,更加高效的表達我心中設想的“模型”。這是一種在概念級別的優化,而程序的短小精悍只是它的一種“表象”。就像是整理一團電線,並不是把它們揉成一團然後塞進一個盒子裏就好。這樣的做法只會給你以後的工作帶來更大的麻煩,而且還有安全隱患。

所以我的這種短小往往是在語義和邏輯 層面的,而不是在語法上死摳幾行代碼。我絕不會為了程序顯得短小而讓它變得難以理解或者容易出錯。相反,很多其它人所追求的短小,卻是盲目的而沒有原則的。在很多時候這些小伎倆都只是在語法層面,比如想辦法把兩行代碼“搓”成一行。可以說,這種“片面追求短小”的錯誤傾向,造就了一批語言設計上的錯誤,以及一批“擅長於”使用這些錯誤的程序員。

現在我舉幾個簡單的“片面追求短小”的語言設計。

自增減操作

很多語言裏都有 i++++i 這兩個“自增”操作和 i----i 這兩個“自減”操作(下文合稱“自增減操作”。很多人喜歡在代碼裏使用自增減操作,因為這樣可以“節省一行代碼”。殊不知,節省掉的那區區幾行代碼比起由此帶來的混淆和錯誤,其實是九牛之一毛。

從理論上講,自增減操作本身就是錯誤的設計。因為它們把對變量的“讀”和“寫”兩種根本不同的操作,毫無原則的合並在一起。這種對讀寫操作的混淆不清,帶來了非常難以發現的錯誤。相反,一種等價的,“笨”一點的寫法,i = i + 1,不但更易理解,而且在邏輯上更加清晰。

有些人很在乎 i++

++i 的區別,去追究 (i++) + (++i) 這類表達式的含義,追究 i++++i 誰的效率更高。這些其實都是徒勞的。比如,i++++i 的效率差別,其實來自於早期 C 編譯器的愚蠢。因為 i++ 需要在增加之後返回 i 原來的值,所以它其實被編譯為:

(tmp = i, i = i + 1, tmp)

但是在

for (int i = 0; i < max; i++)

這樣的語句中,其實你並不需要在 i++ 之後得到它自增前的值。所以有人說,在這裏應該用 ++i 而不是 i++,否則你就會浪費一次對中間變量 tmp 的賦值。而其實呢,一個良好設計的編譯器應該在兩種情況下都生成相同的代碼。這是因為在 i++

的情況,代碼其實先被轉化為:

for (int i = 0; i < max; (tmp = i, i = i + 1, tmp))

由於 tmp 這個臨時變量從來沒被用過,所以它會被編譯器的“dead code elimination”消去。所以編譯器最後實際上得到了:

for (int i = 0; i < max; i = i + 1)

所以,“精通”這些細微的問題,並不能讓你成為一個好的程序員。很多人所認為的高明的技巧,經常都是因為早期系統設計的缺陷所致。一旦這些系統被改進,這些技巧就沒什麽用處了。

真正正確的做法其實是:完全不使用自增減操作,因為它們本來就是錯誤的設計。

好了,一個小小的例子,也許已經讓你意識到了片面追求短小程序所帶來的認知上,時間上的代價。很可惜的是,程序語言的設計者們仍然在繼續為此犯下類似的錯誤。一些新的語言加入了很多類似的旨在“縮短代碼”,“減少打字量”的雕蟲小技。也許有一天你會發現,這些雕蟲小技所帶來的,除了短暫的興奮,其實都是在浪費你的時間。

賦值語句返回值

在幾乎所有像 C,C++,Java 的語言裏,賦值語句都可以被作為值。之所以設計成這樣,是因為你就可以寫這樣的代碼:

if (y = 0) { ... }

而不是

y = 0;
if (y) { ... }

程序好像縮短了一行,然而,這種寫法經常引起一種常見的錯誤,那就是為了寫 if (y == 0) { ... } 而把 == 比較操作少打了一個 =,變成了 if (y = 0) { ... }。很多人犯這個錯誤,是因為數學裏的 = 就是比較兩個值是否相等的意思。

不小心打錯一個字,就讓程序出現一個 bug。不管 y 原來的值是多少,經過這個“條件”之後,y 的值都會變成 0。所以這個判斷語句會一直都為“假”,而且一聲不吭的改變了 y 的值。這種 bug 相當難以發現。這就是另一個例子,說明片面追求短小帶來的不應有的問題。

正確的做法是什麽呢?在一個類型完備的語言裏面,像 y=0 這樣的賦值語句,其實是不應該可以返回一個值的,所以它不允許你寫:

x = y = 0

或者

if (y = 0) { ... }

這樣的代碼。

x = y = 0 的工作原理其實是這樣:經過 parser 它其實變成了 x = (y = 0)(因為 = 操作符是“右結合”的)。x = (y = 0) 這個表達式也就是說 x 被賦值為 (y = 0) 的值。註意,我說的是 (y = 0) 這整個表達式的值,而不是 y 的值。所以這裏的 (y = 0) 既有副作用又是值,它返回 y 的“新值”。

正確的做法其實是:y = 0 不應該具有一個值。它的作用應該是“賦值”這種“動作”,而不應該具有任何“值”。即使牽強一點硬說它有值,它的值也應該是 void。這樣一來 x = y = 0if (y = 0) 就會因為“類型不匹配”而被編譯器拒絕接受,從而避免了可能出現的錯誤。

仔細想一想,其實 x = y = 0if (y = 0) 帶來了非常少的好處,但它們帶來的問題卻耗費了不知道多少人多少時間。這就是我為什麽把它們叫做“小聰明”。

思考題:

  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);
     }
    

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

    (提示,Google 的代碼規範在這一點上是正確的。為什麽?)

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

     if (condition) {
       return x;
     } else {
       return y;
     }
    

    都被人改成了:

     if (condition) {
       return x;
     }
     return y;
    

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

    (提示,改過之後的代碼不如原來的好。為什麽?)

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

    (提示,註意圖靈機的“讀寫頭”。)

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

【轉】程序語言的常見設計錯誤(1) - 片面追求短小