著名的麻省理工学院教授哈尔-艾贝尔森(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:注释应该消除混乱,而不是造成混乱;

如果没有史蒂文·列维的《黑客:计算机革命的英雄》中的这段故事,关于坏注释的讨论就不完整了。

虽然我和其他人一样的欣赏一个好黑客,但这不是典范。如果你的注释引起了混乱,而不是消除混乱,那就请删除它吧。

规则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/

08-15 15:30