管理进化

好代码为什么需要好注释


虽然有许多资源可以帮助程序员编写更好的代码(例如书籍和静态分析器),但很少有资源可以编写更好的注释。虽然衡量程序中的评论数量很容易,但很难衡量质量,而且两者不一定相关。一个不好的评论比根本没有评论更糟糕。这里有一些规则可以帮助你获得一个快乐的媒介。

麻省理工学院著名教授哈尔·阿贝尔森(Hal Abelson)说:"程序必须写给人们阅读,而且只是偶然地让机器执行。"

虽然他可能故意低估了运行代码的重要性,但他发现程序有两个非常不同的受众。编译器和解释器忽略注释,并发现所有语法正确的程序同样易于理解。人类读者非常不同。我们发现有些程序比其他程序更难理解,我们期待评论来帮助我们理解它们。

虽然有许多资源可以帮助程序员编写更好的代码(例如书籍和静态分析器),但很少有资源可以编写更好的注释。虽然衡量程序中的评论数量很容易,但很难衡量质量,而且两者不一定相关。一个不好的评论比根本没有评论更糟糕。正如彼得·沃格尔所写:

编写然后维护注释是一项费用。

编译器不会检查注释,因此无法确定注释是否正确。

另一方面,您可以保证计算机正在执行您的代码指示它执行的操作。

虽然所有这些观点都是正确的,但走向另一个极端并且从不写评论是错误的。以下是一些规则,可帮助您获得快乐的媒介:

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

规则2:好的评论不能原谅代码不清晰。

规则3:如果您无法编写明确的注释,则代码可能存在问题。

规则4:评论应该消除混淆,而不是造成混淆。

规则5:在注释中解释单一惯用代码。

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

规则7:在最有帮助的地方包括指向外部参考资料的链接。

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

规则 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)自己并不理解它,后来不得不重写它。

这让人想起了Kernighan定律:

调试的难度是编写代码的两倍。因此,如果尽可能巧妙地编写代码,则根据定义,您不够聪明,无法对其进行调试。

警告读者远离你的代码就像打开你汽车的危险信号灯:承认你正在做一些你知道是非法的事情。相反,将代码重写为您足够理解的内容以进行解释,或者更好的是,这很简单。

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

如果没有Steven Levy的《黑客:计算机革命的英雄》中的这个故事,任何对不良评论的讨论都是不完整的:

[Peter Samson]在拒绝在他的源代码中添加注释来解释他在特定时间所做的事情时特别晦涩难懂。Samson编写的一个分发良好的程序继续执行了数百个汇编语言指令,在包含数字1750的指令旁边只有一条注释。评论是RIPJSB,人们绞尽脑汁地思考它的含义,直到有人发现1750年是巴赫去世的那一年,而参孙为《安息》约翰·塞巴斯蒂安·巴赫写了一个缩写。

虽然我和下一个人一样欣赏一个好的黑客,但这并不是典范。如果您的评论导致混淆而不是消除它,请将其删除。

规则 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%。

一个注释器提供了一个优化,该优化已合并到存储库中。

另一位评论者提出了一种避免边缘情况的方法。

将其与此评论进行对比(为了保护有罪者而略有修改):

// 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:包括指向最有帮助的外部参考资料的链接

当然,并非所有引用都指向堆栈溢出。考虑:

// 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:修复错误时添加注释

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

// 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)

虽然可用于查找添加或修改行的提交,但提交消息往往是简短的,并且最重要的更改(例如,修复问题#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 Tell You How, Comments Tell You Why。"

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

推荐阅读:

1、人工智能能否代替码农

2、“Hello, World”程序需要什么

智齿客服