注釋,是一種“必須的惡”。
若編程語言有足夠表達(dá)力,或我們長于用這些語言來表達(dá)意圖,就不那么需要注釋--也許根本不需要。
注釋的恰當(dāng)用法是彌補(bǔ)我們?cè)谟么a表達(dá)意圖時(shí)的失敗。
注意,注釋畢竟是一種失敗,是我們無法找到不用注釋就能表達(dá)的辦法,所以才用注釋。
如果發(fā)現(xiàn)自己需要寫注釋,想想看是否有辦法用代碼來表達(dá)。
每次用代碼表達(dá),都應(yīng)該夸獎(jiǎng)一下自己。每次寫注釋,都應(yīng)該做個(gè)鬼臉,感受一下自己在表達(dá)能力上的失敗。
因?yàn)槌绦騿T不能堅(jiān)持維護(hù)注釋,所以注釋可能會(huì)撒謊。存在的時(shí)間越久,離所描述的代碼越遠(yuǎn)。
例如以下例子,因?yàn)橹虚g插入了代碼,注釋和所要描述的代碼距離很遠(yuǎn):
<pre>
MockRequest request;
private final String HTTP_DATE_REGEXP =
"[SMTWF][a-z]{2},s[0-9]{2}s[JFMASOND][a-z]{2}s"+
"[0-9]{4}s[0-9]{2}:[0-9]{2}:[0-9]{2}sGMT"; private Response response;
private FitNesseContext context;
private FileResponder responder;
private Locale saveLocale;
// Example: "Tue, 02 Apr 2003 22:18:49 GMT"
</pre>
一,注釋不能為了美化糟糕代碼
寫注釋常見的動(dòng)機(jī)是糟糕的代碼的存在。
我們編寫了一個(gè)模塊,發(fā)現(xiàn)它令人困擾亂七八糟,所以就寫了注釋。
而此時(shí)需要做的,反而是要把代碼清理干凈!
帶有少量注釋的整潔代碼,要比有大量注釋的零碎而復(fù)雜的代碼像樣的多。
二,用代碼來闡述
程序員總是傾向于認(rèn)為代碼不足以解釋行為,但其實(shí)只要多想想,就能找到辦法來用代碼闡述行為。
有時(shí)候只是改變函數(shù)名就可以。
例如如下例子:
<pre>
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
</pre>
改成:
<pre>
if (employee.isEligibleForFullBenefits())
</pre>
是不是一下子就清晰多了,不需要注釋了。
三,好注釋
有些注釋是必須的和需要寫的。
當(dāng)然,真正好的注釋是你想出辦法不去寫注釋。
1,法律信息
版權(quán)和著作權(quán)聲明是必須和有理由在每個(gè)源文件開頭注釋處放置的內(nèi)容。
<pre>
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.
</pre>
這類注釋不應(yīng)是合同或法典。只要有可能,就指向一份標(biāo)準(zhǔn)許可或其他外部文檔,而不要把所有條款放在注釋中。
2,提供信息的注釋
注釋有時(shí)是為了提供基本信息:
<pre>
// format matched kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile("d:d:d* w, w d, d");
</pre>
3,對(duì)意圖的注釋
有時(shí)候,注釋不僅提供了信息,還提供了某個(gè)決定后的意圖:
<pre>
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});
String text = "'''bold text'''";
ParentWidget parent = new BoldWidget(new MockWidgetRoot(), "'''bold 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 (int i = 0; i < 25000; i++) {
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
Thread thread = new Thread(widgetBuilderThread);
thread.start();
}
assertEquals(false, failFlag.get());
</pre>
4,闡釋
有時(shí)候,注釋把某些晦澀難明的參數(shù)或返回值翻譯為某種可讀形式。
通常,更好的方法是盡量讓參數(shù)或返回值自身就足夠清楚,但如果參數(shù)或返回值是某個(gè)標(biāo)準(zhǔn)庫的一部分,或者你不能修改的代碼,幫助闡釋其意義就會(huì)有用。
<pre>
public void testCompareTo() throws Exception {
WikiPagePath a = PathParser.parse("PageA");
WikiPagePath ab = PathParser.parse("PageA.PageB");
WikiPagePath b = PathParser.parse("PageB");
WikiPagePath aa = PathParser.parse("PageA.PageA");
WikiPagePath bb = PathParser.parse("PageB.PageB");
WikiPagePath ba = PathParser.parse("PageB.PageA");
assertTrue(a.compareTo(a) == 0); // a == a
assertTrue(a.compareTo(b) != 0); // a != b
assertTrue(ab.compareTo(ab) == 0); // ab == ab
assertTrue(a.compareTo(b) == -1); // a < b
assertTrue(aa.compareTo(ab) == -1); // aa < ab
assertTrue(ba.compareTo(bb) == -1); // ba < bb
assertTrue(b.compareTo(a) == 1); // b > a
assertTrue(ab.compareTo(aa) == 1); // ab > aa
assertTrue(bb.compareTo(ba) == 1);// bb > ba
}
</pre>
5,警示
有時(shí)候,用于警告其他程序員會(huì)出現(xiàn)某種后果的注釋也是有用的。
<pre>
// Don't run unless you
// have some time to kill.
public void _testWithReallyBigFile() {
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToSend(this);
String responseString = output.toString();
assertSubString("Content-Length: 1000000000", responseString);
assertTrue(bytesSent > 1000000000);
}
</pre>
又例如:
<pre>
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;
}
</pre>
6,TODO注釋
有時(shí),有理由用//TODO形式在源代碼中放置要做的工作列表。
<pre>
//TODO-MdM these are not needed
// We expect this to go away when we do the checkout model protected VersionInfo makeVersion() throws Exception {
return null;
}
</pre>
7,放大
注釋可以用來放大某種看來不合理的代碼的重要性:
<pre>
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()));
</pre>
8,公共API中的Javdoc
良好描述的公共API是非常令人滿意的。如果在編寫公共API,請(qǐng)為它編寫良好的Javadoc。
四,壞注釋
1, 喃喃自語
有時(shí)候,作者太著急或者沒花心思,結(jié)果注釋成了喃喃自語不知所云。
<pre>
public void loadProperties() {
try {
String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
FileInputStream propertiesStream = new FileInputStream(propertiesPath);
loadedProperties.load(propertiesStream);
}
catch(IOException e) {
// No properties files means all defaults are loaded
}
}
</pre>
2,多余的注釋
有的注釋純屬多余,讀注釋花的時(shí)間可能比讀代碼花的時(shí)間長。
<pre>
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception {
if(!closed) {
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
</pre>
3, 誤導(dǎo)性注釋
有時(shí)候,注釋有誤導(dǎo)性,例如前面出現(xiàn)過的這段:
<pre>
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception {
if(!closed) {
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
</pre>
從注釋里看,在this.closed時(shí)函數(shù)會(huì)立即返回,但實(shí)際上,函數(shù)會(huì)有一段休眠時(shí)間然后再判斷。
如果程序員只看了注釋,則很容易就期望this.closed為真時(shí)立即返回,但代碼中就會(huì)有沒有預(yù)期到的延遲。
4,循規(guī)式注釋
所謂每個(gè)函數(shù)都要有Javadoc或者每個(gè)變量都要有注釋的規(guī)矩是很愚蠢的,徒然讓代碼變得散亂。
<pre>
/** *
- @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 = duration; cdList.add(cd);
}
</pre>
5,日志式注釋
有人會(huì)在每次編輯代碼時(shí),在模塊開始處添加一條注釋。
因?yàn)槲覀儸F(xiàn)在有了源碼控制系統(tǒng)(svn,git等)了,這類冗長的記錄只會(huì)讓模塊變得凌亂,應(yīng)當(dāng)全部刪除。
<pre>
- Changes (from 11-Oct-2001)
- 11-Oct-2001 : Re-organised the class and moved it to new package com.jrefinery.date (DG);
- 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate class (DG);
- 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate class is gone (DG);
- Changed getPreviousDayOfWeek(), getFollowingDayOfWeek() and getNearestDayOfWeek() to correct bugs (DG);
- 05-Dec-2001 :Fixed bug in SpreadsheetDate class (DG);
- 29-May-2002 :Moved the month constants into a separate interface (MonthConstants) (DG);
- 27-Aug-2002 :Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
- 03-Oct-2002 :Fixed errors reported by Checkstyle (DG);
- 13-Mar-2003 :Implemented Serializable (DG);
- 29-May-2003 :Fixed bug in addMonths method (DG);
- 04-Sep-2003 :Implemented Comparable. Updated the isInRange javadocs (DG);
- 05-Jan-2005 :Fixed bug in addYears() method (1096282) (DG);
</pre>
6,廢話注釋
有時(shí)候會(huì)看到純?nèi)粫r(shí)廢話的注釋
<pre>
/**
- Default constructor. /
protected AnnualDateRule() { }
</pre>
<pre>
/* The day of the month. /
private int dayOfMonth;
</pre>
<pre>
/* - Returns the day of the month. *
- @return the day of the month. */
public int getDayOfMonth() {
return dayOfMonth;
}
</pre>
7,Javadoc廢話
Javadoc也可能是廢話。下列Javadoc來自某知名開源庫,其注釋的目的是什么?
<pre>
/** The name. /
private String name;
/* The version. /
private String version;
/* The licenceName. /
private String licenceName;
/* The version. */
private String info;
</pre>
8,能用函數(shù)或變量時(shí)就別用注釋
例如以下代碼和注釋:
<pre>
// does the module from the global list <mod> depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
</pre>
可以改為:
<pre>
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
</pre>
9,位置標(biāo)記
有時(shí),程序員喜歡在源代碼中標(biāo)記某個(gè)特別位置,但基本上都無用。
<pre>
// Actions //////////////////////////////////
</pre>
10,括號(hào)后面的注釋
有時(shí)程序員會(huì)在深度嵌套結(jié)構(gòu)的函數(shù)中,在括號(hào)后面放置特殊的注釋。
但如果你發(fā)現(xiàn)想標(biāo)記右括號(hào),往往要做的是縮短函數(shù)(見函數(shù)一章)。
<pre>
public class wc {
public static void main(String[] args) {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String line;
int lineCount = 0;
int charCount = 0;
int wordCount = 0;
try {
while ((line = in.readLine()) != null) {
lineCount++;
charCount += line.length();
String words[] = line.split("W");
wordCount += words.length;
} //while
System.out.println("wordCount = " + wordCount);
System.out.println("lineCount = " + lineCount);
System.out.println("charCount = " + charCount);
} // try
catch (IOException e) {
System.err.println("Error:" + e.getMessage());
} //catch
} //main
}
</pre>
11,歸屬與署名
例如:
<pre>
/* Added by Rick */
</pre>
這是源代碼控制系統(tǒng)做的事情。
如果你寫注釋在那里,隨著一年又一年的過去,注釋就會(huì)越來越不準(zhǔn)確,越來越與原作者沒關(guān)系。
12,注釋掉代碼
把代碼注釋掉,并遺留在那里是很討厭的行為。而我們有了源代碼控制系統(tǒng),一定不要留下這些遺留物。
<pre>
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));
</pre>
13,注釋中有HTML標(biāo)簽
注釋中的HTML標(biāo)簽會(huì)讓注釋很難讀。
如果需要呈現(xiàn)網(wǎng)頁,那也應(yīng)該由工具生成標(biāo)簽,而不是寫在注釋里。
14,非本地信息
如果一定要寫注釋,請(qǐng)確保它描述了離它最近的代碼,別給出其他地方代碼的信息。
例如以下代碼給出了一個(gè)默認(rèn)端口,而該端口不是由此段代碼所能控制。
如果設(shè)默認(rèn)端口的代碼被修改,此注釋就變成了誤導(dǎo)。
<pre>
/**
- Port on which fitnesse would run. Defaults to <b>8082</b>. *
- @param fitnessePort
*/
public void setFitnessePort(int fitnessePort) {
this.fitnessePort = fitnessePort;
}
</pre>
15,信息過多
別再注釋中添加有趣的歷史性話題和無關(guān)的細(xì)節(jié)描述,這些只會(huì)增加閱讀代碼的負(fù)擔(dān)。
以下注釋除了RFC文檔編號(hào)外,其他細(xì)節(jié)對(duì)讀者完全沒必要。
<pre>
/*
RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
Part One: Format of Internet Message Bodies
section 6.8. Base64 Content-Transfer-Encoding
The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters. Proceeding from left to right, a 24-bit input group is formed by concatenating 3 8-bit input groups. These 24 bits are then treated as 4 concatenated 6-bit groups, each of which is translated into a single digit in the base64 alphabet. When encoding a bit stream via the base64 encoding, the bit stream must be presumed to be ordered with the most-significant-bit first. That is, the first bit in the stream will be the high-order bit in the first 8-bit byte, and the eighth bit will be the low-order bit in the first 8-bit byte, and so on.
*/
</pre>
16,不明顯的聯(lián)系
注釋及其描述的代碼之間的聯(lián)系應(yīng)該顯而易見,以下注釋就有些問題:
<pre>
/*
- 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];
</pre>
以上注釋和代碼里,過濾的字節(jié)指的是什么?與+1還是*3有關(guān)系?為什么用200?
這個(gè)注釋就是失敗的,因注釋本身還需要更多的注釋。
17,函數(shù)頭的注釋
短函數(shù)不需要太多描述。為只做一件事的短函數(shù)取個(gè)好名字,往往比寫函數(shù)頭的注釋要好。
18,非公共代碼中的Javadoc
雖然Javadoc對(duì)于公共API很有用,但對(duì)于不打算作為公共用途的代碼就很沒必要了。對(duì)此類代碼加Javadoc形式的注釋幾乎等同于寫八股文章。
問題:
1,你寫注釋的風(fēng)格是什么樣的?犯了以上哪些錯(cuò)誤?