【翻譯】編寫程式碼註釋的最佳實踐

peida發表於2021-08-15

著名的麻省理工學院教授哈爾-艾貝爾森(Hal Abelson)曾說過:
程式碼首先是寫給人看的,只是計算機拿去執行了而已

雖然他可能故意的低估了計算機執行程式碼的重要性,但他說的是非常正確的。我們的成型有兩個非常不同的受眾。編譯器和直譯器不會關注程式碼的註釋,對於計算機來說,所有語法正確的程式都是同樣的容易理解的。而對於閱讀程式碼的人來說,則完全是不一樣的。我們發現有些的程式程式碼非常難以理解,我們希望通過增加註釋來幫我們閱讀。

有很多資源可以幫助程式設計師們寫出更好的程式碼,例如圖書文件和靜態程式碼分析工具等。但是如何才能寫出更好的程式碼註釋的資源卻很少。雖然,我們可以很容易的度量程式中註釋的數量,但很難去度量其質量,而且這兩者之間也不存在必然的聯絡。一個糟糕的註釋比沒有註釋更加糟糕。

正如Peter Vogel所寫的:

  • 編寫和維護註釋是一項開支。
  • 你的編譯器並不檢查你的註釋,所以沒有辦法確定註釋是否正確有效。
  • 另一方面,你可以保證計算機完全按照你的程式碼的要求來執行。

雖然所有這些觀點都是正確的,但如果走到另一個極端,從不寫註釋,那就是一個錯誤。

這裡有一些基本的規則,可以幫助你提升寫註釋的能力:

  1. 規則1:註釋不應該於程式碼重複;
  2. 規則2:好的註釋不能作為程式碼不清晰的藉口;
  3. 規則3:如果你不能寫出一個清晰的註釋,那麼你的程式碼可能也是有問題的;
  4. 規則4:註釋應該消除混亂,而不是造成混亂;
  5. 規則5:在註釋中解釋不規範的程式碼;
  6. 規則6:提供複製的程式碼的原始來源的連結;
  7. 規則7:最最有幫助的地方加入外部參考資料的連結;
  8. 規則8:在修復bug時新增註釋;
  9. 規則9:使用註釋來標記不完整的實現;

下面是對以上9條規則的詳細解釋,結合具體的案例來接入如何在實際編碼中應用他們。

規則1:註釋不應該於程式碼重複

許多初級程式設計師在程式碼中寫了太多的註釋。因為他們在初學程式碼是被老師訓練成這樣。

例如很多人在每個閉合的大括號上都加上一行註釋,已表明那個程式碼塊要結束。

if (x > 3) {
   …
} // if

還有更嚴重的要求,在每行程式碼上都要加上註釋。雖然這對初學者來說可能是一個有效的措施,但這樣的註釋習慣,就像孩子學習騎自行車的輔助輪一樣,是最終需要放棄的。

不能增加任何資訊的註釋是負面價值的東西,應為他們:

  • 增加了視覺混亂;
  • 浪費了讀寫的時間;
  • 可能會過時;

典型的一個壞列子如下:

i = i + 1;         // Add one to i

註釋不附加任何有效的資訊,但產生了維護成本。

要求對每行程式碼都寫註釋的規則,在Reddit上受到了嘲諷:

// create a for loop // <-- comment
for // start for loop
(   // round bracket
    // newline
int // type for declaration
i    // name for declaration
=   // assignment operator for declaration
0   // start value for i

規則2:好的註釋不能作為程式碼不清晰的藉口;

註釋的另外一個被誤用,就是提供了本應該在程式碼中出現的資訊。一個簡單的例子,有人用一個字母來命名一個變數,然給新增一個註釋來描述變數的用途:

private static Node getBestChildNode(Node node) {
    Node n; // best child node candidate
    for (Node node: node.getChildren()) {
        // update n if the current state is better
        if (n == null || utility(node) > utility(n)) {
            n = node;
        }
    }
    return n;
} 

其實,更好的變數命名可以消除對註釋的需要:

private static Node getBestChildNode(Node node) {
    Node bestNode;
    for (Node currentNode: node.getChildren()) {
        if (bestNode == null || utility(currentNode) > utility(bestNode)) {
            bestNode = currentNode;
        }
    }
    return bestNode;
} 

正如 Kernighan和 Plauger 在《程式設計風格的要素》中寫道:"不要註釋壞的代,而是重寫它"。

規則3:如果你不能寫出一個清晰的註釋,那麼你的程式碼可能也是有問題的

在Unix原始碼中最臭名昭著的註釋是:你不應該理解這一點。她出現在一些毛茸茸的上下文切換程式碼之前。丹尼斯·裡奇 (Dennis Ritchie) 後來解釋說,它的目的是“本著‘這不會出現在考試中’的精神,而不是無禮的挑戰。” 不幸的是,事實證明,他和合著者肯·湯普森 (Ken Thompson) 自己並不理解,後來不得不重寫。

這讓人想起克尼漢定律

除錯一段程式碼的難度是編寫它們的兩倍,因此如果你的程式碼寫的儘可能巧妙,按照定義而言,你可能沒有能力來除錯它了。

警告閱讀遠離你的程式碼,就像開啟你的汽車的危險訊號燈:承認你正在做的事情是非法的。相反,將程式碼重寫為你能很好理解並易解釋的,最好是簡單直接的。

規則4:註釋應該消除混亂,而不是造成混亂;

如果沒有史蒂文·列維的《黑客:計算機革命的英雄》中的這段故事,關於壞註釋的討論就不完整了。

[Peter Samson] 拒絕在他的原始碼中新增註釋來解釋他在特定時間所做的事情,這一點尤其晦澀。Samson 編寫的一個分佈廣泛的程式繼續執行數百條組合語言指令,在包含數字 1750 的指令旁邊只有一個註釋。註釋是 RIPJSB,人們絞盡腦汁思考它的含義,直到有人發現 1750 是巴赫去世的那一年,而Samson寫的是Rest In Peace Johann Sebastian Bach的縮寫。

雖然我和其他人一樣的欣賞一個好黑客,但這不是典範。如果你的註釋引起了混亂,而不是消除混亂,那就請刪除它吧。

規則5:在註釋中解釋不規範的程式碼

對其他人可能認為不需要或者多餘的程式碼進行註釋是一個好主意,例如來自App Inventor 的程式碼:

final Object value = (new JSONTokener(jsonString)).nextValue();
// Note that JSONTokener.nextValue() may return
// a value equals() to null.
if (value == null || value.equals(null)) {
    return null;
}

如果沒有註釋,有人可能會簡化程式碼,或者將其視為神祕但必不可少的咒語。通過寫下為什麼需要好程式碼來節省未來閱讀者的時間和焦慮。

需要判斷程式碼是否需要註釋,在學習Kotlin的時候,遇到過一個Android教程中的程式碼,形式如下:

if (b == true)

我馬上想到是否可以替換為:

if (b)

就像在 Java 中所做的那樣。經過一些研究,我瞭解到可空布林變數明確地與 true 進行比較,以避免醜陋的空檢查:

if (b != null && b)

因此,我建議不要對常見習語的去寫註釋,除非是專門為新手編寫的教程。

規則6:提供複製的程式碼的原始來源的連結;

如果你像大多數程式設計師一樣,有時會使用在網上找到的程式碼。包括對來源的引用使未來的讀者能夠獲得完整的上下文,例如:

  • 正在解決什麼問題
  • 誰提供了程式碼
  • 為什麼推薦該解決方案
  • 評論者是怎麼想的
  • 它是否仍然有效
  • 如何改進
    例如,請考慮以下注釋:
/** Converts a Drawable to Bitmap. via https://stackoverflow.com/a/46018816/2219998. */

按照註釋中連結中資訊可以看出:

  • 該程式碼的作者是Tomáš Procházka,他在Stack Overflow上排名前3%。
  • 一個評論者提供了一個優化方法,已經被納入到repo中。
  • 另一個評論者提出了一個避免邊緣情況的方法。

與此評論形成鮮明對比的是(為保護犯錯者,稍作改動)。

// Magical formula taken from a stackoverflow post, reputedly related to
// human vision perception.
return (int) (0.3 * red + 0.59 * green + 0.11 * blue);

任何想要了解上面程式碼的人都將不得不去搜尋查詢公式。貼上 URL 比稍後查詢引用要快得多。

一些程式設計師可能不願意表明他們沒有自己編寫程式碼,但重用程式碼可能是一個明智的舉動,既節省了時間,又讓你獲得了更多關注。當然,你永遠不應該貼上您不理解的程式碼。

人們從 Stack Overflow 問題和答案中複製了大量程式碼。該程式碼屬於需要署名的知識共享許可。引用註釋就滿足該要求。

同樣地,您=你應該參考那些有幫助的教程,以便可以再次找到它們,並感謝他們的作者:

// Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html
// for a great reference and examples.

規則7:最最有幫助的地方加入外部參考資料的連結

當然,並非所有引用都指向了 Stack Overflow,可以考慮:

// http://tools.ietf.org/html/rfc4180 suggests that CSV lines
// should be terminated by CRLF, hence the \r\n.
csvStringBuilder.append("\r\n");

到標準和其他文件的連結可以幫助讀者理解你的程式碼正在解決的問題。雖然這些資訊可能會出現在設計檔案中,但恰當的註釋會在何時何地提供給讀者最需要的資訊。在這種情況下,跟隨連結表明RFC 4180已經被RFC 7111更新,這是有用的資訊。

規則8:在修復bug時新增註釋

不僅應該在最初編寫程式碼時新增註釋,還應該在修改程式碼時新增註釋,尤其是在修復錯誤時。考慮這個註釋:

 // NOTE: At least in Firefox 2, if the user drags outside of the browser window,
  // mouse-move (and even mouse-down) events will not be received until
  // the user drags back inside the window. A workaround for this issue
  // exists in the implementation for onMouseLeave().
  @Override
  public void onMouseMove(Widget sender, int x, int y) { .. }

註釋不僅幫助讀者理解當前和引用的方法中的程式碼,還有助於確定是否仍然需要該程式碼以及如何測試它。

也可以幫助問題修復的跟進:

// Use the name as the title if the properties did not include one (issue #1425)

雖然git blame可用於查詢新增或修改行的提交,但提交訊息往往很簡短,並且最重要的更改(例如,修復問題 #1425)可能不是最近提交的一部分(例如,移動從一個檔案到另一個檔案的方法)。

規則9:使用註釋來標記不完整的實現

有時即使程式碼有已知的缺陷,也有必要簽入程式碼。雖然不共享程式碼中已知的缺陷可能很誘人,但最好使這些缺陷明確,例如使用 TODO 註釋:

// TODO(hal): We are making the decimal separator be a period, 
// regardless of the locale of the phone. We need to think about 
// how to allow comma as decimal separator, which will require 
// updating number parsing and other places that transform numbers 
// to strings, such as FormatAsDecimal

對此類註釋使用標準格式有助於衡量和解決技術債務。更好的是,向你的跟進列表中新增一個問題,並在你的註釋中引用該問題。

結論

我希望上面的例子已經表明註釋不會成為錯誤程式碼的藉口或修復;它們通過提供不同型別的資訊來補充良好的程式碼。正如 Stack Overflow 聯合創始人 Jeff Atwood 所寫,“程式碼告訴你如何,註釋告訴你為什麼。

遵循這些規則應該可以節省你和你的隊友的時間和挫折感。

最後,我確信這些規則並非詳盡無遺,並期待在評論中看到更多的建議和補充。

原文https://stackoverflow.blog/2021/07/05/best-practices-for-writing-code-comments/

相關文章