編寫代碼註釋的最佳實踐

作者 | Ellen Spertus

譯者 | 王雪迎   

出品 | CSDN(ID:CSDNnews)

雖然有很多資源可以幫助程序員編寫更好的代碼,比如書籍或靜態分析器,但是很少有資源可被用於編寫更好的註釋。雖然度量一個程序中的註釋數量很容易,但衡量註釋的質量卻很難,而且兩者並不一定相關。差的註釋比根本不寫註釋更糟糕。這裏有一些規則可以幫助你實現一種折中的方法。

麻省理工學院的著名教授 Hal Abelson 曾說過:“程序必須寫給人們閱讀,而只是附帶地讓機器執行。” 雖然他可能故意低估了運行代碼的重要性,但卻注意到了程序有兩種截然不同的受衆。編譯器和解釋器會忽略註釋,找出同等容易理解的所有語法正確的程序。而人類讀者則完全不同。我們發現有些程序比其它的更難理解,此時就會通過查看註釋來幫助我們理解這些程序。

雖然有很多資源可以幫助程序員編寫更好的代碼,比如書籍或靜態分析器,但是很少有資源可被用於編寫更好的註釋。雖然度量一個程序中的註釋數量很容易,但衡量註釋的質量卻很難,而且兩者並不一定相關。差的註釋比根本不寫註釋更糟糕。正如 Peter Vogel 所述:

  1. 編寫並維護註釋是一項開銷。

  2. 編譯器不會檢查註釋,因此無法確定註釋是否正確。

  3. 另一方面,你可以保證計算機完全按照你的代碼所示運行。

所有這些觀點都是正確的,但如果走到另一個極端,即從不寫註釋,那將是一個錯誤。這裏有一些規則可以幫助你實現一種折中的方法:

本文其餘部分將逐條解釋這些規則,提供示例並說明如何以及何時應用它們。

規則 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 自己也理解不了,後來不得不重寫。

這讓人想起了 Kernighan 定律:

調試在一開始就比編寫程序困難一倍。因此,按照定義,如果你的代碼寫得非常巧妙,那麼你就沒有足夠的能力來調試它。

警告讀者遠離你的代碼就像打開汽車的危險警示燈:承認你正在做知法犯法的事情。相反,把代碼重寫成你充分理解的東西,或者更進一步,直截了當地進行解釋。

規則 4:註釋應該消除混亂,而不是引起混亂

如果沒有 Steven Levy 的 黑客:計算機革命的英雄 中的這個故事,任何關於負面註釋的討論都是不完整的:

[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 中一樣。經過一點研究,我瞭解到可以爲 null 的布爾變量會顯式地與 true 進行比較,以避免出現難看的 null 檢查:

if (b != null && b)

我建議不要爲常見的語法添加註釋,除非專門爲新手編寫教程。

規則 6:提供複製代碼的原始出處鏈接

如果你像大多數程序員一樣,有時會使用網上找到的代碼。包含對源代碼的引用,可使將來的讀者能夠獲得完整的上下文,例如:

例如,考慮此註釋:

/** Converts a Drawable to Bitmap. via https://stackoverflow.com/a/46018816/2219998. */

點開鏈接,給出了以下信息:

將其與下面的註釋對比(稍作修改以防止侵權):

// 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 時添加註釋

不僅應該在最初編寫代碼時添加註釋,修改代碼時,特別是修復 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/

聲明:本文由 CSDN 翻譯,轉載請註明來源。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Jb2LPuMhR3Jl0jLbITcAWg