“Notes on Programming in C” 閱讀 (精簡版)

寫在前面: 之前有一個完整的版本,包含了原文、翻譯和評註的版本。但感覺這樣很影響閱讀,所以又寫了一個精簡版,只包含部分翻譯和個人評註。

“Notes on Programming in C” 一文是 羅布·派克 (Rob Pike) 於 1989 年寫的一份關於 C 語言編程的編程實踐建議,包含 9 個主題的簡要說明,涵蓋了代碼風格、程序優化、設計模式等內容。

該文雖然是針對 C 語言所寫,並且年代久遠,但其中的很多想法對編寫高質量的代碼現在看來仍然具有非常好的指導意義。

此外,正如文中 “Introduction” 部分所說,

… 我並不希望每個人都贊同文中所述的內容,因爲這只是一些 “見解”,而 “見解” 將隨着時間而變 … (如果你反對我的想法) 如果這裏內容使你思考你爲何而反對,那麼 (比起全盤照收) 更好。絕不要我說應該如何做你就怎樣編程,應該按照你認爲的完成該程序的最好的方法去進行編程

這也是我想和閱讀本文的讀者想說的。

0. 大綱

  1. Issues of typography: 代碼格式 (推薦閱讀指數 2)
  2. Variable names: 變量命名 (推薦閱讀指數 2)
  3. The use of pointers: 指針的使用 (推薦閱讀指數 1)
  4. Procedure names: 函數命名 (推薦閱讀指數 2)
  5. Comments: 註釋 (推薦閱讀指數 3)
  6. Complexity: 代碼複雜度 (這應該是這篇文章最有名的一部分) (推薦閱讀指數 4)
  7. Programming with data: 面向數據編程 (推薦閱讀指數 2.5)
  8. Function pointers: 函數指針 (推薦閱讀指數 1.5)
  9. Include files: include 文件 (推薦閱讀指數 0.5)

之所以沒有五星推薦,是因爲即便是最有名的章節,其中的觀點放在現在也都是非常廣爲人知的了

1. Issues of typography: 代碼格式

程序應該被視作是供程序員閱讀的出版物(但卻不應該墨守成規),通俗地講就是讓其他人理解起來不費勁兒。

同時,整個團隊的代碼風格的一致性是比清晰而規範的格式更加重要的。(本質上這也是爲了讓代碼閱讀起來更加容易)

2. Variable names: 變量命名

對於變量命名,真正起決定性的應該是表達的清晰性,而非名稱的長短。

如果是一個很少被用到的全局變量,叫一個如 maxphysaddr 這種較長的名字還沒有什麼問題。但對於一個循環中每一行都會被用到的數組的索引 (index),沒有什麼比 i 更加合適的了。給這種變量命名爲 index 或者 elementnumber 只會讓你輸入更多字符而沒有其他的意義,並且使得代碼變得更加晦澀。一般情況下,變量名越長,代碼邏輯閱讀與梳理就變得越費勁,例如:

for(i=0 to 100)
    array[i]=0
---- v.s. -----
for(elementnumber=0 to 100)
    array[elementnumber]=0

真實的代碼比這個例子更加糟糕(我就曾經因爲過分追求變量名稱的意義將一個簡單的變量弄得讀起來非常費勁)。索引就僅僅是一個表示,保持 i 這種命名風格就好。

變量命名除了清晰可理解外,剩下的準則就只有代碼前後的風格一致性了。例如,如果你的一個變量被命名爲 maxphysaddr,不要將另一個變量命名爲 lowestaddress (正確的應該是 minphyaddr)

就我個人 (原作者) 而言,我是更喜歡較短的名稱的,那些沒有在變量中體現的含義,可以從上下文中容易地推測出來。

然而全局變量,因爲缺少上下文信息,所以應該命名中所包含的信息儘量全一些,這就是爲何全局變量我會命名成 maxphysaddr 這種風格;而局部變量,我則簡單地命名爲 np。當然名稱長短的問題僅僅是個人品味,但有時候個人的品味卻關乎代碼的清晰性。

有的小夥伴要問了,索引都用 i 命名,最後不就是 i, j, l, m, n… 了麼。我們思考一個問題,如果代碼已經多層循環到了索引命名都很困難了,是不是循環過深了呢?是不是函數承載的功能過多呢?是不是應該考慮重構現有代碼呢?

3. The use of pointers: 指針的使用

原作者例舉了指針的一些好處,但無論如何,我個人傾向於指針是邪惡的
指針之所以容易出錯,本質上是因爲指針相比於其他代碼的語法在邏輯上是更加抽象的,人類的大腦沒有辦法一直準確地處理這種抽象。
如果一個程序中充滿了複雜的指針運算,調試和閱讀起來會非常艱難。
這也是爲什麼大多數更高級的編程語言都拋棄了指針。

另外,編寫程序時如果能使用易於理解的代碼特性實現一些功能,不要使用那些理解起來很困難的語法,這種困難很可能是因爲這種語法比較抽象,需要更多的大腦負荷;而增加大腦負荷,只會使開發者無法聚焦到應該聚焦的事情上。

不過有些人天生覺得指針這一類東西很好理解也很好用,這些人真的是很幸運。

4. Procedure names: 函數命名

函數的命名應該反映其功能,同時也應該體現出其返回值的形式。因爲函數是使用在表達式中的,經常被用於 if 語句中,所以函數的名字需要被合理的命名,例如

if(checksize(x))

checksize 沒有辦法幫我們推斷出當 x 不合理時,這個函數到底是返回 true 還是 false,取而代之的,

if(validsize(x))

validsize 就可以讓我們明確的知道其返回邏輯,使用起來也將更少出錯。

5. Comments: 註釋

原作者由於以下原因傾向於儘量少地去寫註釋:

  • 第一,好的代碼結構清晰,命名規範,本身就是自解釋的,不需要額外的註釋
  • 第二,註釋無法被編譯器檢測,所以無法保證你含義的正確性,尤其是代碼被修改了以後。誤導的註釋會讓人對代碼的邏輯感到困惑
  • 第三,註釋使代碼變的凌亂不堪

但也不是從來不寫任何註釋。最多的情況是,對接下來的一段代碼的作用作出簡介。例如,

  • 對全局變量的解釋;
  • 對一個不是很合乎常理或者一個關鍵函數的簡介;
  • 一大段計算結尾的標誌

除非是核心數據結構的前面,不要出現大團大團的註釋 (對於數據的註釋絕對比對算法的註釋重要得多)。基本而言,避免寫註釋就對了;如果你覺得你的代碼非要寫註釋才能被理解,那最好重構成更容易理解的代碼,見下一節 “Complexity: 代碼複雜度”

努力提高代碼的自解釋性比大段的註釋重要得多

另外,我在實際開發中經常發現代碼中存在 IDE 自動生成的註釋,例如:

/**
 * 從一個JSON數組得到一個java對象集合,其中對象中包含有集合屬性
 * @param object
 * @param clazz
 * @param map
 * 
 * @return
 */
public static <T,K> List<?> getDTOList(String jsonString, Class<T> clazz, Map<String,K> map) {
...
}

因爲代碼迭代,上面的例子中第一個 @param object 和實際簽名無法對應,而且即便可以對應,這種把函數簽名重寫一遍的操作有什麼意義?只會讓 IDE 提示高亮影響代碼的閱讀!
IDE提示

6. Complexity: 代碼複雜度

絕大多數的代碼都過於複雜了,或者說,比高效地解決需求所需要的代碼複雜度要複雜。

爲何會這樣?大多數情況下這是因爲一個糟糕的代碼設計 (架構),但這裏我不想討論關於代碼設計 (架構) 這一話題,這個話題太大太廣了。

排除架構不談,即便是一些微小的方面,大多數代碼還是過於複雜,遵循下面的原則可以在一定程度上避免複雜代碼的出現:

  • 規則 1: 你無法真正地知道一個程序真正把資源花費在哪裏,真正的性能瓶頸總是在一些意想不到的地方。所以不要一而再再而三地瞎猜然後修改代碼,除非你十分確定性能瓶頸真正發生在什麼地方
  • 規則 2: 測量。在你能準確測量代碼各部分性能之前不要操之過急;即便你已經測量準確了,如果一部分代碼和其他部分比起來性能不是特別的差,最好也不要進行優化
  • 規則 3: 當 n 很小的時候,那些所謂 “高效” 的算法通常性能比較差,而現實中 n 通常是小的。除非你知道在你的實際場景中 n 經常會是很大的,不要使用那些看起來非常吸引人的複雜算法 (在優化前要先看一眼上面的規則 2)。例如,在日常工作中,二叉樹 (binary trees) 往往比伸展樹 (splay trees) 性能更好
  • 規則 4: 與簡單的算法比起來,吸引人的 “高效” 算法要花費更多精力才能實現,然而卻往往更容易產生 bug。所以儘量使用簡單的算法和簡單的數據結構。
    在日常的使用中,下面的數據結構絕對夠用了
    text 數組: array 鏈表: linked list 哈希表: hash table 二叉樹: binary tree
    當然你要把這些數據合理的組織成複合數據。例如,一個符號表 (Symbol Table,應該就是字典) 需要由哈希表以及數組的鏈表複合而成。
  • 規則 5: 數據驅動。如果你選擇了正確的數據結構並且數據之間組織得足夠良好,對應的算法幾乎是不言自明的。程序的核心是數據結構而非算法 (參考 Brooks 人月神話 102 頁,數據的表現形式是編程的根本)
  • 規則 6: 沒有規則 6 (我理解是所有的東西都要儘量保持簡潔)

個人感覺作者想表達的總結起來就是

  • 不要憑空地優化,不要貿然地優化
  • 儘量簡化你的程序:用一句時髦的話叫簡單可依賴
  • 數據先於算法:有助於提高代碼質量和開發、維護效率

7. Programming with data: 面向數據編程

與一大堆 if else 相比,算法經常可以通過數據被更加緊湊、高效、清晰的表達出來。這是因爲,如果我們要處理的複雜性,是由於一系列相互獨立細節的排列組合引起的,這個排列組合本身就是可以被編碼簡化的,對應的複雜性也是可以被簡化的。
一個典型的例子是分析表 (parsing tables,編譯原理相關,見 wiki),即將編程語言複雜的語法規則轉化爲固定的、相對簡單的語法,從而使其可以被運行。有限狀態機特別適用於這個艱鉅的工作。幾乎任何輸入是抽象類型、輸出是獨立行爲的解析類的程序,都可以通過數據驅動來進行編程。

這種程序設計思路最讓人覺得神奇的地方是,一個程序的驅動表有時是由另一個程序動態生成的,在編譯器的例子中,後者稱爲解析生成器 (parser generator)。
一個比較接地氣的例子是,某個操作系統通過一系列的表來進行驅動,這些表正確將 I/O 請求連接到合適的設備驅動,而這些表的配置正是由一個數據驅動的程序來完成的:該程序讀入連接到該系統的一系列特定設備的描述信息,並生成了這些驅動表。

8. Function pointers: 函數指針

這一部分主要是對 C 語言而言。函數指針也是公認的 C 語言比較難以掌握的功能,不合理的使用將使得代碼可讀性非常差,引發各種潛在的、難以調試的 BUG。

不過作者的核心論點是,將函數作爲變量一樣操作,可以方便的進行面向對象抽象,進而使得 C 語言更容易開發和維護。雖然以上這些功能面嚮對象語言都可以方便地支持。

脫離 C 語言這一情景,作者這裏提到的 “協議” (protocol) 的概念在各種語言中都得到了廣泛的應用,例如 Java 語言可以將接口作爲參數進行傳遞,python 中函數和類都是 “first class” ,以及鴨子類型等。

函數指針這一節說的實際上是面向 “協議” (protocol) 的編程,常見的說法是面向接口而非面向實現編程。這使得程序的抽象能力得到了增強;而抽象意味着固定、不變,即面對新的需求,代碼將不需要修改,只需要進行擴展就行了。

這也是爲什麼作者一直在強調 “使用函數指針來對代碼的複雜性進行封裝”。

9. Include files: include 文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章