查尔斯.普雷斯特.斯科特(Charles Prestwich Scott):
评论是自由的,但事实是神圣的
注释可以将优秀的代码和糟糕的代码区分开来,将艰涩难懂的逻辑与清晰友好的算法区分开。但我们也无须过分夸大注释的作用。如果说你的代码是蛋糕,那么你的注释就如同蛋糕上糖衣,它被精巧地涂抹以增加蛋糕的美感和价值,但不能掩饰蛋糕的裂缝和瑕疵。
一、什么是代码注释
- 从语法角度看,注释是编译器将忽略不计的源代码。
- 从语义角度看,注释是对其所处位置的代码的注解。
注释的作用有多种:
1) 强调某个特殊的问题领域;
2)在头文件中记录媒介;
3)描述某个算法,协助维护程序;
4)分割各个函数以帮组快速浏览整个源文件。 - 注释是内部的文档化机制
二、注释标记
- C语言的注
/*
开头,*/
结束,可以跨多行。 - C++、C99、C#和Java语言增加了以
//
作为开头的单行注释。 - 其他语言也提供了类似的块注释和行注释功能,语法可能不同。
三、多少注释是恰当的
小威廉.斯特伦克(William Strunk Jr.):
铿锵有力之文必简洁
- 把重点放在注释的质量上,而不是数量上;比编写多少注释更重要的是注释的内容。
- 只在能为代码增加色彩时才编写注释。优秀的演奏家追求用最少的演奏创造出最美的声音。
- 真正好的代码并不需要注释,它们能自我解释。像
f()
和g()
这样的函数名才会大声嚷嚷,要求注释说明它们,但是像someGoodExample()
这样的函数名根本不需要注释。
四、注释中应该有什么
贺瑞斯(Horace):
写好源头和本源是明智的做法
不好的注释比没有注释更糟糕,它们歪曲事实进而误导读者。
解释为什么,而不是怎么样
(a).注释不是用来描述程序是怎样运行的,代码自己是其最权威的描述。
(b).注释应该描述代码块起什么作用。
写/* update WidgetList structure from GibWLRegistry*/
这样的注释,不如写/* cache widget information for later*/
。注释同一段代码,后者表达代码目的,而前者只告诉你这段代码做了什么。
(c).注释可用来解释为什么代码要这么写。如有两种可供选择的实现方式,你决定采用其中一种,也许你该使用注释来解释一下,为什么你选择了这种实现方式。不要取代或重复代码
(a).如果注释描述的内容可以由编程语言本身实现,那么就试着用具体语句表达它。
(b).如果你发现你用大量注释解释某个算法如何运行,请赶快停止,然后考虑是否需要重构代码或算法。
(c).不要用注释描述变量的用法,重命名变量。
(d).如果你要文档化一个应当总是成立的条件,也许你该写一个断言确保注释有用
注释的作用是标注和解释代码。注释需要满足:
(a).记录意想不到的内容。如果代码有一部分是不常见、意想不到的,用注释记录下来;如果有特定问题需要规避,如一个操作系统问题,应该在注释中说明。
(b).讲真话。不要写不准确的注释。
(c).清晰明了。不要让注释模棱两可,内容尽量具体;不一定是完整的、语法正确的句子,但必须是可读的;避免缩写。
(d).只关注当前代码,避免分心。不要记录过去我们怎么做;不要把应当剔除的代码(如旧代码)包含在注释中;避免使用ASCII艺术;不要在代码块结尾添加多余的注释,如在if
语句的后括号后注释//end if
。
每当写完注释后,在代码上下文中回顾一遍这些注释,考虑一下:它们是否都是正确的信息,它们是否简洁,它们是否容易理解。
五、实践
看看下面一小段C++代码:
for (int i = 0; i < wlst.sz(); ++i)
k(wlst[i])
很明显,你根本搞不懂这是在干吗。应用合理排版规则和添加注释说明对它进行改进:
// Iterate over all widgets in the widget list
for (int i = 0; i < wlst.sz(); ++i)
{
// print out this widget
k(wlst[i]);
}
现在好多了,起码这段代码的目的清晰了。但它还是不太令人满足。使用恰当的函数名和变量重写代码:
for (int i = 0; i < widgets.size(); ++i)
printWidget(widgets[i]);
再看代码,发现根本不需要注释,代码本身能自我描述了。
这印证了:
不要文档化差劲的代码——重写这些代码
六、注释的一些细节及其作用
根据你自己的品味,把下面的建议当作指导性的原则:
一致性。
所有的注释应该清晰明了,前后一致。为你的注释选择一种特定的布局方式,始终坚持使用。清晰的块注释
将首位标记(如C/C++中/*
和*/
)各自放在一行,凸显它们;让块注释左侧多缩进一个字符,让注释显的像一个整体。如
/*
* This is
* a block
* comment.
*/
而不是这种
/*
This is
a block
comment.
*/
- 缩进的注释
注释不应该截断代码或打乱逻辑流程。让注释与对应的代码的缩进一样。
看到下面这样的代码,无疑更费劲:
void strangeCommentStyle()
{
for (int i = 0; i < JUST_ENOUGH; ++i)
{
//This is a meaning comment about the next line.
doSomethingMeaningful(i);
// good comment
doOtherOperation(i);
}
}
在不带括号的循环中,不要把注释放在单循环体语句之前,这会导致灾难性后果。
- 行尾注释
大多数注释都是放在各自的行上,但有时较短的单行注释可以跟在代码语句后头。这种情况下,在注释与代码之间留白是一种好习惯。例如:
Class Point
{
public:
........
private:
int x; //X轴坐标值
int y; //Y轴坐标值
int z; //Z轴坐标值
};
如果行尾注释直接跟在声明后面,代码看起来会显得拥挤,读起来更费劲。
- 选择一种维护成本较低的风格
可以把更多时间放在编写代码而不是摆弄注释上。
- 帮组阅读代码
a).注释通常在它描述的代码上方,而不是下方。读者一般都是从上向下阅读,这样注释可以让读者为即将读到的内容做好准备。
b).注释后面紧跟代码,代码后面放一个空白行,接下来是下一个注释代码段。这种代码与空白一起使用的方式,可以将代码分隔成"段落",益于阅读。
- 分隔板
注释经常被用作不同代码部分之间的“分隔板”。如在一个若干个类或函数的源文件中,可能有以下注释:
/******************************************
* class foo implementation
******************************************/
或
////////////////////////////////////////////////////
我们应当避免大量使用这种"分隔板"类的注释。将代码分组的应该是良好的缩进和结构,而不是生动的ASCII艺术。
标志
注释还可以用作代码中内嵌的标志。如:
a).//XXX
用来标识棘手的代码或需要重新编写的代码;
b).//FIXME
表示已知已经损坏的代码;
c).//TODO
用来标识缺少一些功能,需要日后返工。文件头注释
a).所有源文件都应该以描述其内容的注释块作为开头。这个头注释是一个概述前言,提供了文件的关键信息。
b).头注释应该包含文件的目的、版本、所有权及版权声明;
c).头注释不需要把文件中定义的所有函数、类、全局变量等内容罗列,太累赘。帮组编写程序
a).一种常见的编码方式是首先用注释构造代码结构,然后在各行注释下面填写代码。这种方式要注意及时删除一些无用的注释和修改一些不恰当的注释。
b).另一种常见的编码方式是徒手编写代码,再添加注释。可能常常忘记某项工作或不能写出好的注释。更好的方式是边写代码边写注释,实践会告诉你写多少注释。注释过时
要明白注释会过时。当你修正、添加或修改任何代码时,请同时修正、添加或修改相应的注释。
八、总结
德尔莫.施瓦茨(Delmore Schwartz):
重要的写作是要讲眼之所见,这样就不必一遍又一遍的口口相传了
写高质量的注释比写高数量的注释更好,然而,最好的是写出高质量的代码——那些无需注释的自文档化代码。