编写代码注释的最佳实践

作者: 刘星
日期: 2021年12月29日

我们每天写代码,做Code Review,努力提高代码质量。 尽管已经有很多资源可以帮助程序员们编写更好的代码(例如书籍和静态分析器),但很少有资源可以帮助编我们写出更好的注释。 衡量一个程序中的注释数量很容易,衡量质量却很难。而且数量不代表质量。无用的注释比没有注释更糟糕。本文将为你带来一些写出更好的注释的最佳实践。

麻省理工学院著名教授哈尔·阿贝尔森 (Hal Abelson) 说:“程序应该是为人们阅读而编写的,只是偶然地供机器执行”。

虽然他说的有点夸张,但他的这句话也阐明了程序有两种截然不同的受众:机器和人类。编译器和解释器忽略注释,所有语法正确的程序对于他们来说都是一样容易理解。人类则是非常不同的。我们很多时候我们会遇到一些难以理解的程序,我们希望通过注释来帮助我们理解它们。

糟糕的注释比没有注释更糟糕。正如彼得沃格尔所写的:No Comment: Why Commenting Code Is Still a Bad Idea 有以下观点

1. 编写然后维护注释将会花费你的精力。 2. 您的编译器不会检查您的注释,因此无法确定注释是否正确。 3. 另一方面,计算机完全按照您的代码执行的操作而不是注释。

虽然所有这些观点都是正确的,但走到另一个极端并且从不写注释是错误的。虽然整洁的代码会说话,但是在很复杂的代码中可能说的也不会那么清楚。

这里有一些规则可以帮助您更好的写代码注释:

规则1:注释不应该重复代码

规则2:好的注释也不应该成为你糟糕的代码的借口

规则3:如果不能写出清晰的注释,可能是你的代码有问题

规则 4:注释应该消除混乱,而不是造成混乱

规则 5:在注释中解释单语代码

规则 6:提供指向复制代码原始来源的链接

规则 7:在需要的地方包含指向外部参考的链接

规则 8:在修复错误时添加注释

规则 9:使用注释来标记不完整的实现

下文将逐一解释了这些规则,提供了示例并解释了如何应用它们。

规则1:注释不应该重复代码。

许多初级程序员写了太多的注释,因为他们在入门时被教导要多写注释。甚至我见过高年级计算机科学课程的学生在每个封闭的大括号中添加注释以指示哪个块结束:

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

我还听说过教师要求学生对每一行代码进行注释。虽然这对于未入门的初学者来说可能是一个合理的教导,但这样的注释就像自行车的训练轮一样,在学会后骑自行车时应该去掉。

没有价值的注释反而是会起负面作用的,因为它们:

1. 让你眼花缭乱 2. 花费你宝贵的时间来写和读 3. 改下代码就可能会过时

典型的坏例子是:

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

https://www.reddit.com/r/ProgrammerHumor/comments/5dhdt6/my_teacher_told_me_i_needed_to_comment_every_line/da56d6m

规则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 在《The Elements of Programming Style》 中所写,“Don’t comment bad code — rewrite it.”

规则3:如果不能写出清晰的注释,可能是你的代码有问题。

Unix 源代码中最臭名昭著的注释是“You are not expected to understand this” (你不需要理解这个),它出现在一些复杂的上下文切换代码之前。丹尼斯·里奇 (Dennis Ritchie) 后来解释说,它的目的是“本着‘这不会出现在考试中’的精神,而不是无礼的挑衅。”不幸的是,事实证明,他和合著者肯·汤普森自己并不理解,后来不得不重写。

这让人想起柯林汉定律

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.

调试的难度是最初编写代码的两倍。因此,如果您尽可能聪明地编写代码,根据定义,您就不够聪明来调试它。

(Brian Kernighan)

https://github.com/dwmkerr/hacker-laws#kernighans-law

警告读者远离您的代码就像打开汽车的危险信号灯:表明您正在做的事情是非法的。此时也许更好的是将代码重写以便于您足够理解的内容,然后再留下明确的注释。

规则 4:注释应该消除混乱,而不是造成混乱

如果没有史蒂文·列维的《黑客:计算机革命的英雄》这个故事,对糟糕注释的讨论就不会完整:

[Peter Samson] 拒绝在他的源代码中添加注释来解释他在特定时间所做的事情,这一点尤其晦涩。 Samson 编写的一个分布良好的程序继续执行数百条汇编语言指令,在包含数字 1750 的指令旁边只有一个注释。注释是 RIPJSB,人们绞尽脑汁想它的含义,直到有人发现 1750 是巴赫去世的那一年,萨姆森写下了安息约翰·塞巴斯蒂安·巴赫(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 中所做的那样。经过一番研究,我了解到Nullable Boolean显式地与 true 进行比较,可以避免像下面这种丑陋的空检查:

if (b != null && b)

建议不要包含对这种常见用法的注释,除非专门为新手编写教程

规则 6:提供指向复制代码原始来源的链接。

如果您像大多数程序员一样,有时会使用在网上找到的代码。包括对来源的引用使未来的读者能够获得完整的上下文,例如:

1. 正在解决什么问题 2. 谁提供了代码 3. 为什么推荐该解决方案 4. 注释者是怎么想的 5. 是否仍然有效 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");

标准和其他文档的链接可以帮助读者了解您的代码正在解决的问题。尽管此信息可能位于设计文档中的某个位置,但位置恰当的注释可为读者提供最需要的时间和地点的指示。

规则 8:在修复错误时添加注释。

不仅应该在最初编写代码时添加注释,还应该在修改代码时添加注释,尤其是在修复错误时。考虑这个注释:

// 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 所写,Code Tells You How, Comments Tell You Why (代码告诉你如何,注释告诉你为什么)

良好的遵循这些规则应该可以节省您和您的队友的时间。

我确信这些规则并非详尽无遗,并期待在注释中看到建议的补充。

文档信息

版权声明:署名-非商业性使用-禁止演绎 4.0 国际(CC BY-NC-ND 4.0)

原文链接:https://www.liuxing.io/blog/best-practices-for-writing-comments/

发表日期:2021年12月29日


最近更新