技術翻譯 - 編寫程式註解的最佳實踐

Best practices for writing code comments from StackOver flow

著名的麻省理工學院教授 Hal Abelson 說:「必須編寫程式供人們閱讀,而只是偶然的也供機器執行。」雖然這句話可能會當作故意低估了運行程式碼的重要性,但他發現程式碼有兩種截然不同的受眾。編譯器和解釋器會忽略註解並發現所有語法正確的程式同樣容易理解。人類來閱讀則是非常不同的。我們發現有些程式比其他程式更難理解,我們希望通過註解來幫助我們理解它們。

我們有許多的資料來幫助工程師寫出更好的程式碼(例如書籍或靜態掃描),但卻很少在說明如何寫更好的註解。雖然衡量程式中註解的數量很容易,但很難衡量質量,而且兩者不一定相關。一個不好的註解比沒有註解更糟糕。正如 Peter Vogel 所寫

  1. 編寫然後維護註解是一項花費
  2. 您的編譯器不會檢查您的註解,因此無法確定註解是否正確。
  3. 另一方面,您可以保證電腦正在按照您的程式碼告訴它執行的操作。

儘管所有這些觀點都是正確的,但走另一個極端而不寫註解是錯誤的。這裡有一些規則可以成為幫助你快樂的媒介:

  • Rule 1: Comments should not duplicate the code.
    註解不應該重複程式碼
  • Rule 2: Good comments do not excuse unclear code.
    好的註解不應該成為不清晰程式碼的藉口
  • Rule 3: If you can’t write a clear comment, there may be a problem with the code.
    如果不能寫清楚的註解,可能是程式碼有問題
  • Rule 4: Comments should dispel confusion, not cause it. 註解應該消除混亂,而不是引起混亂
  • Rule 5: Explain unidiomatic code in comments.
    在註解中解釋單一的程式碼
  • Rule 6: Provide links to the original source of copied code.
    提供複製程式碼的來源的連結
  • Rule 7: Include links to external references where they will be most helpful.
    包括外部參考的連結會是相當有幫助的
  • Rule 8: Add comments when fixing bugs.
    修復錯誤時添加註解
  • Rule 9: Use comments to mark incomplete implementations.
    使用註解來標記不完整的實作。

文章餘下部分將解釋了這些規則,並提供範例和解釋如何以及何時應用它們。

Rule 1: Comments should not duplicate the code

許多新手工程師會寫太多的註解,因為他們的入門講師訓練他們這麼做。我見過高年級的計算機科學課程的學生在每個右括號中增加註解來比較哪個區外結束:

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

Rule 2: Good comments do not excuse unclear code

註釋的另一種濫用是提供本應在程式碼中的訊息。一個簡單的例子是當有人用一個字母命名一個變數,然後增加一個描述其用途的註解:

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 在 The Elements of Programming Style 中所寫,「Don’t comment bad code — rewrite it.」

Rule 3: If you can’t write a clear comment, there may be a problem with the code

Unix 原始碼中最惡名昭著的註解是 "You are not expected to understand this",它出現在一些雜亂的 context-switching 程式碼之前。Dennis Ritchie 後來解釋說,它的目的是 "in the spirit of ‘This won’t be on the exam,’ rather than as an impudent challenge." 不幸的是,事實證明他和協作者 Ken Thompson 自己也並不理解它,後來不得不重寫它。

這讓人想起 Kernighan’s Law

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.
首先,除錯的難度是編寫程式碼的兩倍。因此,如果您盡可能巧妙地編寫程式碼,那麼根據定義,您還不夠聰明,無法對其進行除錯。

警告讀者遠離你的程式碼就像打開汽車的警示燈:承認你正在做你知道是非法的事情。取而代之的是,將程式碼重寫成你足以理解到可以解釋的東西,或者更好的是,成為一種直覺。

Rule 4: Comments should dispel confusion, not cause it

如果沒有 Steven Levy 的 Hackers: Heroes of the Computer Revolution 中的這個故事,任何關於差勁的註解的討論都是不完整的:

[Peter Samson] was particularly obscure in refusing to add comments to his source code explaining what he was doing at a given time. One well-distributed program Samson wrote went on for hundreds of assembly-language instructions, with only one comment beside an instruction that contained the number 1750. The comment was RIPJSB, and people racked their brains about its meaning until someone figured out that 1750 was the year Bach died, and that Samson had written an abbreviation for Rest In Peace Johann Sebastian Bach.
[Peter Samson] 拒絕在他的原始碼中增加註解來解釋,他在給定時間所做的事情特別晦澀難懂。Samson 編寫的一個分佈良好的程式繼續執行數百條組合語言指令,在包含行數 1750 的指令旁邊只有一條註解。註解是 RIPJSB,人們絞盡腦汁想知道它的含義,直到有人發現 1750 是巴赫去世的那一年,Samson 寫了《安息吧約翰·塞巴斯蒂安·巴赫》的縮寫。

雖然我和下一個人一樣欣賞一個好的駭客,但這並不是好的模範。如果您的註解會導致混淆而不是弭平它,請將其刪除。

Rule 5: Explain unidiomatic code in comments

註解別人可能認為不需要或多餘的程式碼是個好主意,例如來自 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 中所做的那樣。經過一番研究,我了解到為了避免醜陋的空值檢查,Nullable 的 Boolean 變數會與 truefalse 做比較:

if (b != null && b)

我建議要包含常見語法糖的註解,除非專門是為新手編寫教學。

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

  • 解決了什麼問題
  • 誰提供了代碼
  • 為什麼推薦該解決方案
  • 評論者怎麼想的
  • 是否仍然有效
  • 如何改進

例如,參考以下註解:

/** 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.

當然,並不是所有的引用都指向 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 更新(有用的信息)。

Rule 8: Add comments when fixing bugs

不僅應該在最初編寫程式碼時新增註解,而且在修改程式碼時,尤其是修復錯誤時也應該添加註解。參考這個註解:

// 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) { .. }

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

對於 issue trackers 也很有幫助:

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

雖然 git blame 可用於查詢新增或修改行的 commit,但 commit message 往往很簡短,並且最重要的更改(例如,fixing issue #1425)可能不是最近 commit 的一部分(例如,將方法從一個文件移動到另一個文件)。

Rule 9: Use comments to mark incomplete implementations

有時即使程式碼有資訊的限制,但還是有必要檢查代碼。雖然不分享程式碼中已知的缺陷可能很誘人,但最好明確指出這些缺陷,例如使用 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

此類註解使用標準格式有助於衡量和解決技術債問題。更好的是,向您的 issue trackers 增加一個 issue,並在您的註解中引用該 issue。

總結

我希望上面的範例已經表明註解並不是一個藉口或修正錯誤的程式碼;它們透過提供不同類型的資訊來補充好的程式碼。正如 Stack Overflow 聯合創始人 Jeff Atwood 所寫的那樣,"Code Tells You How, Comments Tell You Why."

遵循這些規範應該可以節省您和您隊友的時間和挫敗感。

也就是說,我確信這些規則並不詳盡,並期待在評論中看到建議的補充(還有其他地方嗎?)。

Reference

Did you find this article valuable?

Support 攻城獅 by becoming a sponsor. Any amount is appreciated!