别给糟糕的代码添加注释——重新写吧。
若编程语言足够有表达力,或者我们长于用这些语言来表达意图,就不那么需要注释——也许根本不需要。
注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。如果你发现自己需要写注释,再想想看是否有办法通过代码来表达。
本文不提倡写太多注释,能用代码表达就不要写注释。
1. 注释不能美化糟糕的代码
写注释的常见动机之一是糟糕的代码的存在。
带有少量注释的整洁而有表达力的代码,比带有大量注释的零碎而复杂的代码像样的多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。
2. 用代码来阐述
有时候,代码本身不足以解释其行为。幸运的是,很多时候我们都可以把代码写地能解释自己的行为。
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_DAY) &&
(employee.age > 65))
改成这样怎么样?
if (employee.isEligibleForFullBenefits())
3. 好注释
有些注释是必须的,也是有利的。不过要记住,唯一真正的好注释是你想办法不去写的注释。
3.1 法律信息
版本及著作权声明就是有理由在每个源文件开头注释处放置的内容
3.2 提供信息的注释
// format matched kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile("\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");
注释说明,该正则表达式意在匹配一个经由SimpleDateFormat.format函数利用特定格式字符串格式化的时间和日期。
这类注释有时管用,但更好的方式是尽量利用函数名称表达信息。
3.3 对意图的解释
有时,注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。
下面的例子,你也许不同意程序员给这个问题提供的解决办法,但至少你知道他想干什么。
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder =
new WidgetBuilder(new Class[] {BoldWidget.class});
String text = "'''bold text'";
ParentWidget parent =
new BoldWidget(new MockWidgetRoot(), text);
AtomicBoolean failFlag = new AtomicBoolean();
failFlag.set(false);
// This is our best attempt to get a race condition
// by creating large number of threads
for (itn i = 0; i < 25000; i++) {
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
new Thread(widgetBuilderThread).start();
}
assertEquals(false, failFlag.get());
}
3.4 阐释
有时候参数或者返回值晦涩难懂的时候,用注释来帮助阐释其含义就会有用。但前提是,你考虑一下是否有更好的办法,在小心的加上注释。
3.5 警示
有时,用于警示其他程序员会出现某种后果的注释也是有用的。
public static SimpleDateFormat makeStandardHttpDateFormat() {
// SimpleDateFormat is not thread safe
// so we need to create each instance independently.
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df;
}
上面例子也许有更好的解决办法,不过这个注释却是非常有必要的。它能阻止某个急切的程序员以效率之名使用静态初始器。
3.6 TODO注释
TODO是一种程序员认为应该做,但由于某些原因目前还没做的工作。它可能是要提醒删除某个不必要的特性,或者要求他人注意某个问题,它可能是恳请别人取个好名字等等。
如今大多数IDE都提供了定位TODO注释,定期查看这些注释,删除不再需要的,让代码整洁。
3.7 放大
注释可以用来放大某种看起来不合理之物的重要性。
String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting
// spaces that could cause the item to be recognized
// as another list
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
3.8 公共API中的Javadoc
如果你在编写公共API,就该为它编写良好的Javadoc。
4. 坏注释
大多数注释都属此类。通常,坏注释都是糟糕代码的支撑或借口,或者对错误决策的修正,基本上等于程序员自说自话。
4.1 喃喃自语
写一些只有作者自己知道含义的注释,就是喃喃自语。
4.2 多余的注释
相比代码本身无法提供更过的信息就是多余的注释。比如没有证明代码的意义,也没有给出代码的意图或逻辑。读这种注释还不如读代码。
4.3 误导性注释
注释不够精确,和代码本身表达的含义不一致,容易误导程序员。
4.4 循规式注释
所谓每个函数都要有Javadoc或每个变量都要有注释的规矩全然是愚蠢可笑的。这类注释突然让代码变得散乱,满口胡言,令人迷惑不解。
/**
* @param title The title of the CD
* @param author The author of the CD
* @param tracks The number of tracks on the CD
* @param durationInMinutes The duration of the CD in minutes
*/
public void addCD(String title, String author, int tracks, int durationInMinutes) {
CD cd = new CD();
cd.title = title;
cd.author = author;
cd.tracks = tracks;
cd.duration = durationInMinutes;
cdList.add(cd);
}
4.5 日志式注释
有人会在每次编辑代码时,在模块开始处添加一条注释。这类注释就像是一种记录每次修改的日志。
很久以前,在模块开始处创建并维护这些记录还算有道理。那时,我们还没有源代码控制系统可用。如今,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除。
4.6 废话注释
对显然之事喋喋不休,毫无新意。
/**
* Default constructor
*/
protected AnnualDateRule() {
}
/** The day of the month. */
private int dayOfMonth;
/**
* Returns the day of the month
* @return the day of the month
*/
public int getDayOfMonth() {
return dayOfMonth;
}
4.7 可怕的废话
Javadoc也可能是废话。下列Javadoc(摘自某知名开源库)的目的是什么? 答案:无。
/** The name. */
private String name;
/** The version */
private String version;
/** The licenseName */
private String licenseName;
/** The version. */
private String info;
再仔细读读这些注释。你是否发现了剪切-粘贴错误? 如果作者在写(粘贴)注释的时候都没花心思,怎么能指望读者从中受益呢?
4.8 能用函数或变量时就别用注释
看看以下代码概要:
// does the module from the global list depend on the
// subsystem we are part of ?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
可以改成以下没有注释的版本:
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
4.9 位置标记
有时,程序员喜欢在源代码中标记某个特别位置。例如:
// Actions //////////////////////////////////////////
把特定函数放在这种标记下面,多数时候实属无理。鸡零狗碎,理当删除——特别是尾部那一长串无用的斜杠。
这么说吧,如果标记栏不多,就会显而易见。尽量少用这中标记栏。如果滥用,代码就会沉没在背景噪音中,被忽略掉。
4.10 括号后的注释
有时,程序员会在括号后面放置特殊的注释,尽管这对于含有深度嵌套结构的长函数可能有意义,但只会给我们更愿意编写的短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实应该做的是缩短函数。
4.11 归属于署名
/* Added by Rick */
版本控制系统非常善于记住是谁在何时添加了什么。没必要用小小的签名搞脏代码。
4.12 注释掉的代码
直接把代码注释掉是讨厌的做法。别这么干!
InputStreamResponse response = new InputStreamResponse();
resonpse.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultStream);
// response.setContent(reader.read(formatter.getByteCount()));
其他人不敢删除注释掉的代码。他们会想,代码依然放在那儿,一定有其原因,而且这段代码很重要,不能删除。注释掉的代码堆积在一起,就像破旧瓶底的渣滓一般。
曾经有一段时间,注释掉代码可能有用。但我们已经拥有优秀的源代码控制系统,这些系统可以为我们记住不要的代码。我们无需再用注释来标记,删掉即可,它们丢不了。我担保。
4.13 HTML注释
源码中包含HTML注释让人讨厌,难以阅读。
4.14 非本地信息
假如你一定要写注释,请确保它描述了离它最近的代码。别在本地注释的上下文环境中包含出系统级的信息。
下面的例子,除了可怕的冗余之外,还包含了系统级的默认端口信息。但是这个函数完全没控制那个所谓的默认值。假如那个值更改了,无法担保这个注释也会跟着修改。
/**
* port on which fitnesse would run. Default to 8082
* @param fitnessePort
*/
public void setFitnessePort(int fitnessPort) {
this.fitnessePort = fitnessePort;
}
4.15 信息过多
别在注释中添加有趣的历史性话题或者无关的细节描述。
4.16 不明显的联系
注释及其描述的代码之间的联系应该是显而易见。如果你不嫌麻烦要写注释,至少让读者能看着注释和代码,并且理解注释所谈何物。
以来自Apache公共库的这段注释为例
/**
* start with an array that is big enough to hold all the pixels
* (plus filter bytes), and an extra 200 bytes for header info
*/
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
过滤字节是什么鬼? 与那个+1有关系吗?或与*3有关系? 还是与二者皆有关系? 注释的作用是解释未能自行解释的代码。如果注释本身还需要解释,就太遗憾了。
4.17 函数头
短函数不需要太多描述。为只做一件事的短函数取个好名字,通常要比写函数头注释要好。
4.18 非公共代码中的Javadoc
Javadoc对于公共API非常有用,但对于不打算做公共用途的代码就令人厌恶了。为系统中的类和函数生成Javadoc页并非总有用,而Javadoc注释额外的形式要求几乎等同于八股文章。