代码注释是宗教与科技相遇的地方之一。一边是坚信好的代码就是有良好注释的代码的人,另一边是虔诚的最少注释教的信徒。
我们不能在讨论有表现力代码的同时不讨论注释。有表现力的代码是注释的替代方案,还是两者满足不同的需求?这是我想要讨论的主题,通过说明注释在什么情况下是有用的,并补充其他情况下好的代码。
我之所以花你我的时间来讨论争议性的评论话题,是因为我已经浓缩了相当数量的数据,来自:
- 最近在巴黎软件工艺会议上,其中一个主题是关于如何编写代码的经验交流。顺便说一下,这次聚会非常棒。任何想成为更好的软件开发人员和结识有趣的人都可以在那里度过一段美好的时光。
- 来自Steve McConnell的参考书Code Complete,它实际上用了33页的篇幅来讨论注释的主题,以及其他相关的部分。
- 最后,我对这个话题有自己的体会和思考。
如果要我用两句话来总结的话
如果你真的没有时间,这里有一个综合所有的规则:
想象一下,如果你坐在读你代码的人旁边,你需要告诉他们什么。这就是你需要在注释里写的。
Jonathan Boccara (@JoBoccara) 2017年4月27日
确实,考虑下面这行代码:
if (!entries.empty())
{
...
假设你坐在正在阅读你的代码的人旁边说:“看,在做实际工作之前,我们要检查一些条目”。这个人可能会回答什么? “谢谢,但我会读!” 通过这种方式,你只是在挡他们的路,甚至打断他们的注意力。
注释也是一样。你不会想让他们重复代码所表达的。让代码更有表现力就是让代码传达更多信息。
另一方面,如果一个人打开了一个大的源文件,然后你说:“这个文件实现了程序的某某方面”,这样做就会帮他一个大忙,因为仅仅查看代码,则需要更长的时间才能明白代码的意图。注释也该起到这样的效果。
避免使用解释性注释
还有另一种注释:解释代码所做的事情的注释。它们可以为读者携带有价值的信息,否则读者很难理解代码。但是包含这些注释的代码通常是糟糕的代码,因为它是不清楚的,因为需要解释。
通常给出的建议是,以不同的方式编写这段代码,使其更具表现力。有很多方法可以做到这一点,这就是为什么我将Fluent C++奉献给这个主题的原因。
在编写新代码时,让它讲述故事当然是有意义的。但是我不确定这条关于解释性注释的建议在所有情况下都是现实的。
想象一下,你正在进行一个错误修复,但遇到了难以理解的不清楚的代码。你是要打断你的工作,然后改变它吗?这是不可能的。或者记录下来,以便以后进行重构?你真的要对每一段这样的代码都这样做吗?这可能是巨大的工作。相反,留下总结你的发现的注释,对每个人来说都是一个捷径。
而且,有些代码不属于你。一些解释性注释告诉我们,有些代码是这样做的,因为在堆栈的下面,有一些扭曲的东西迫使我们相应的扭曲自己。但是你可能无法访问罪魁祸首代码!因此,解释性注释在我看来是有存在的理由的。
现在有一些捷径可以摆脱一些解释性的注释,比如摆脱魔数。考虑以下注释代码:
// checks that the level of XP is valid
if (xpLevel <= 100)
{
...
它有一个魔数值100,这是一个糟糕的实践。注释行笨拙地试图弥补这一点。这可以迅速变成:
static const int MaximumXPLevel = 100;
if (xpLevel <= MaximumXPLevel)
{
...
因此,解释性的注释是多余的。
在意图层面上交谈
显著提高代码表达性的一件事是提高抽象级别。即使你希望代码处于正确的抽象级别,注释也可以在其中扮演次要的角色。
在Code Complete的第9章中,Steve McConnell展示了伪代码编程过程的技术。这包括在注释中开始编写你想要用英语完成的函数代码。完成后,插入c++(或与此相关的任何语言)的实际代码行,并自然地与注释交织在一起。然后去掉代码中的一些冗余注释,留下解释代码意图的注释。
为了实现这一点,注释应该位于函数的抽象级别。换句话说,他们需要表达代码打算做什么,而不是代码如何实现它。这里有更多关于抽象内容、抽象方式和抽象级别的内容。
还有另一个层次的意图,代码很难说明:为什么。为什么代码是这样实现的,而不是用另一种设计?如果你曾经尝试过一种设计,结果发现它不起作用,这对代码维护者(可能就是你自己)来说是很有价值的信息,可以让他远离错误的轨道。如果有人确实在错误的轨道上,遇到了一个bug并进行了修复,那么在注释中包含一个对问题中的bug ticket的引用可能是有用的。
如果你阅读Make your functions functional,你会看到全局变量通过引入隐式的输入和输出来破坏函数,这些输入和输出是函数可以访问但没有在原型中声明的。原型旁边的注释表明函数与全局变量有什么交互,这可能是一个很好的提示,直到函数被修正。
在注释中记录的另一个有价值的意图是,出于某种(好的)原因,你做出了违背通常是最佳实践的有意识的决定。如果你什么都不提,很有可能之后有人会“修复”它。
Debian系统中的一个bug就说明了这一点,当有人“错误地”删除了一个未初始化的变量时,这个bug产生了巨大的影响。事实证明,这种非初始化参与了身份验证密钥中的随机数生成。Oops。
“注释不会更新”
这是反注释教的有力论据。没有什么强制代码维护者使注释与代码保持一致,这是事实。这样,注释可能会与代码不同步,并转化为误导性的信息。每个人都同意没有注释比错误的注释更好。
这是真的,但有一些建议可以减少这种情况发生的机会。
第一个是在意图层次上注释,因为意图的变化并不像意图的实际实现变化那么频繁。
第二种方法是让注释尽可能接近相应的代码。实际上,没有更新的注释并非出自恶意程序员之手。有时候我们就是不注意那些注释。Steve McConnell甚至建议将变量名标记到注释中,这样当搜索变量出现时,注释也会显示出来。
最后,最后一个技巧是在不经常更改的地方添加注释,通常是在文件的开头,以描述这个文件是关于什么的。即使文件中的实现可能发生变化,该文件涵盖的主题也会在很长一段时间内保持稳定。