【翻譯】編寫代碼注釋的最佳實踐

peida 2021-08-15 13:48:04 阅读数:166

本文一共[544]字,预计阅读时长:1分钟~
最佳

著名的麻省理工學院教授哈爾-艾貝爾森(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/

版权声明:本文为[peida]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815134734597H.html