在编程过程中,编写优质的代码注释对提升代码的可读性和维护性至关重要。虽然有很多资源可以帮助程序员编写更好的代码,例如书籍和静态分析工具,但针对如何编写优质注释的资源却比较少。注释的数量容易衡量,而质量则难以评估,两者之间也不一定有直接关系。实际上,糟糕的注释可能比没有注释还要糟糕。以下这些规则将帮助你在注释和代码之间找到平衡点。

著名的 MIT 教授 Hal Abelson 曾说:“程序必须为人类阅读而写,只有附带地为机器执行。”这句话强调了程序有两类不同的读者。编译器和解释器忽略注释,只要语法正确,它们就认为程序是一样易于理解的。但人类读者却不一定如此,我们常常发现有些程序难以理解,这时候注释的作用就显得尤为重要。

如何写好代码注释

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

许多初学者因为在入门课程中被教导写注释,结果往往在代码的每个大括号后面都加上注释,标明块的结束位置:

if x > 3 {
   // some code
} // if

这样的注释增加了视觉混乱,耗费了编写和阅读的时间,并且容易过时。典型的坏例子是:

i = i + 1 // 将i加1

这样的注释没有提供任何有用的信息,反而增加了维护成本。

规则 2:好的注释不能成为写不清楚代码的借口

注释应该提供代码中没有的背景信息。例如,使用单字母变量名并添加注释来说明它的用途:

func getBestChildNode(node *Node) *Node {
    var n *Node // 最佳子节点候选者
    for _, child := range node.Children {
        // 如果当前状态更好则更新n
        if n == nil || utility(child) > utility(n) {
            n = child
        }
    }
    return n
}

通过更清晰的变量命名,可以避免使用注释:

func getBestChildNode(node *Node) *Node {
    var bestNode *Node
    for _, currentNode := range node.Children {
        if bestNode == nil || utility(currentNode) > utility(bestNode) {
            bestNode = currentNode
        }
    }
    return bestNode
}

正如《编程风格要素》中的 Kernighan 和 Plauger 所说:“不要为糟糕的代码写注释——重写它。”

规则 3:如果无法写出清晰的注释,可能是代码本身存在问题

Unix 源码中有一句最臭名昭著的注释是“你不需要理解这段代码”,这段注释出现在复杂的上下文切换代码之前。Dennis Ritchie 后来解释说,这注释的本意是“这段代码不会出现在考试中”,而不是挑衅。不幸的是,连他和合著者 Ken Thompson 自己也没能理解这段代码,最终不得不重写。

这让人想起 Kernighan 的法则:

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.

调试代码的难度是编写代码的两倍。因此,如果你尽可能聪明地编写代码,那么从定义上来说,你就无法足够聪明地来调试它。

规则 4:注释应消除困惑,而非制造困惑

Steven Levy 的《黑客:计算机革命的英雄》讲述了一个故事:

Peter Samson 拒绝在他的源代码中添加解释当前操作的注释。他写的一个包含数百条汇编指令的程序只有一个注释,注释内容是 RIPJSB,而这一指令的值是 1750。有人绞尽脑汁试图理解其含义,直到有人发现 1750 是巴赫去世的年份,而 RIPJSB 是“愿约翰·塞巴斯蒂安·巴赫安息”的缩写。

尽管这样的注释令人印象深刻,但并不是一个好的示范。如果注释只会引起混淆而不是消除困惑,就应该将其删除。

规则 5:解释非惯用代码

注释应解释那些其他人可能认为不必要或冗余的代码,例如:

value := (new JSONTokener(jsonString)).nextValue()
// JSONTokener.nextValue() 可能返回一个等于 null 的值。
if value == nil || value == interface{}(nil) {
    return nil
}

如果没有注释,某人可能会“简化”代码或将其视为神秘但必需的咒语。通过编写注释来说明代码的必要性,为后续读者节省时间和精力。

判断代码是否需要解释时需慎重。如果是常见的惯用法,无需注释,除非是写给新手的教程。

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

许多程序员会使用网上找到的代码。提供来源可以让后续读者获取完整的上下文,例如:

// 将 Drawable 转换为 Bitmap,代码来自 https://stackoverflow.com/a/46018816/2219998。

通过链接,读者可以了解代码作者是谁,背景是什么,评论者的建议等。避免让读者自己去搜索公式或代码,直接粘贴 URL 更为方便。

在复制代码时,应理解其含义,避免直接粘贴不理解的代码。

规则 7:在最有帮助的地方包含外部引用链接

不仅是 Stack Overflow 的链接,标准文档等也应在适当的位置引用:

// http://tools.ietf.org/html/rfc4180 建议CSV行应以CRLF结尾,因此使用 \r\n。
csvStringBuilder.WriteString("\r\n")

这些链接帮助读者理解代码解决的问题,设计文档中的信息也应在需要时通过注释指明。

规则 8:修复 bug 时添加注释

不仅在编写代码时要添加注释,修改代码尤其是修复 bug 时也应添加注释。例如:

// 注意:在 Firefox 2 中,如果用户将鼠标拖出浏览器窗口,则不会接收到 mouse-move(甚至 mouse-down)事件,
// 直到用户将鼠标拖回窗口内。onMouseLeave() 方法实现了一个变通方法来解决这个问题。
func onMouseMove(sender *Widget, x, y int) {
    // 实现代码
}

这样的注释有助于理解当前和相关方法中的代码,也有助于判断代码是否仍然需要以及如何进行测试。

也可以参考 Issue 跟踪器:

// 如果属性中没有标题,则使用名称作为标题(Issue #1425)

规则 9:用注释标记未完成的实现

有时需要提交代码,即使它还有已知的不足之处。为了明确这些不足,可以使用 TODO 注释来标记,例如:

// TODO: 目前我们只支持使用点号作为小数分隔符,未来需要支持逗号作为小数分隔符,
// 这将需要更新数字解析和其他将数字转换为字符串的地方,例如 FormatAsDecimal。

使用标准格式的注释有助于度量和解决技术债务。最好在 Issue 跟踪器中添加问题,并在注释中引用问题。

总结

希望以上例子能说明注释不能作为糟糕代码的借口;它们应该通过提供不同类型的信息来补充优质代码。正如 Stack Overflow 联合创始人 Jeff Atwood 所说:“代码告诉你如何做,注释告诉你为什么。”

遵循这些规则应能节省团队的时间和减少挫折。

当然,这些规则并不详尽,期待在评论区看到更多建议。


也可以看看