人們看到最好的作家有時(shí)并不理會(huì)修辭學(xué)的規(guī)則。還好,當(dāng)他們這樣做雖然付
出了違反常規(guī)的代價(jià),讀者還經(jīng)常能從句子中發(fā)現(xiàn)某些具有補(bǔ)償性的價(jià)值。除非作
者自己也明確其做法的意思,否則最好還是按規(guī)矩做。
William Strunk和E. B. White,《風(fēng)格的要素》
下面這段代碼取自一個(gè)許多年前寫的大程序:
i f ( (country == SING) I I (country == BRNI) I I
(country == POL) I I (country == ITALY) )
{
/*
* I f the country is Singapore, Brunei or Poland
* then the current time is the answer time
* rather than the off hook time.
* Reset answer time and s e t day of week.
* /
...
這段代碼寫得很仔細(xì),具有很好的格式。它所在的程序也工作得很好。寫這個(gè)系統(tǒng)的程序員會(huì)
對(duì)他們的工作感到驕傲。但是這段摘錄卻會(huì)把細(xì)心的讀者搞糊涂:新加坡、文萊、波蘭和意大
利之間有什么關(guān)系?為什么在注釋里沒有提到意大利?由于注釋與代碼不同,其中必然有一個(gè)
有錯(cuò),也可能兩個(gè)都不對(duì)。這段代碼經(jīng)過了執(zhí)行和測(cè)試,所以它可能沒有問題。注釋中對(duì)提到
的三個(gè)國(guó)家間的關(guān)系沒有講清楚,如果你要維護(hù)這些代碼,就必須知道更多的東西。
上面這幾行實(shí)際代碼是非常典型的:大致上寫得不錯(cuò),但也還存在許多應(yīng)該改進(jìn)的地方。
本書關(guān)心的是程序設(shè)計(jì)實(shí)踐,關(guān)心怎樣寫出實(shí)際的程序。我們的目的是幫助讀者寫出這
樣的軟件,它至少像上面的代碼所在的程序那樣工作得非常好,而同時(shí)又能避免那些污點(diǎn)和
弱點(diǎn)。我們將討論如何從一開始就寫出更好的代碼,以及如何在代碼的發(fā)展過程中進(jìn)一步改
進(jìn)它。
我們將從一個(gè)很平凡的地方入手,首先討論程序設(shè)計(jì)的風(fēng)格問題。風(fēng)格的作用主要就是
使代碼容易讀,無論是對(duì)程序員本人,還是對(duì)其他人。好的風(fēng)格對(duì)于好的程序設(shè)計(jì)具有關(guān)鍵
性作用。我們希望最先談?wù)擄L(fēng)格,也是為了使讀者在閱讀本書其余部分時(shí)能特別注意這個(gè)問
題。
寫好一個(gè)程序,當(dāng)然需要使它符合語法規(guī)則、修正其中的錯(cuò)誤和使它運(yùn)行得足夠快,但
是實(shí)際應(yīng)該做的遠(yuǎn)比這多得多。程序不僅需要給計(jì)算機(jī)讀,也要給程序員讀。一個(gè)寫得好的
程序比那些寫得差的程序更容易讀、更容易修改。經(jīng)過了如何寫好程序的訓(xùn)練,生產(chǎn)的代碼
更可能是正確的。幸運(yùn)的是,這種訓(xùn)練并不太困難。
程序設(shè)計(jì)風(fēng)格的原則根源于由實(shí)際經(jīng)驗(yàn)中得到的常識(shí),它不是隨意的規(guī)則或者處方。代
碼應(yīng)該是清楚的和簡(jiǎn)單的—具有直截了當(dāng)?shù)倪壿?、自然的表達(dá)式、通行的語言使用方式、
有意義的名字和有幫助作用的注釋等,應(yīng)該避免耍小聰明的花招,不使用非正規(guī)的結(jié)構(gòu)。一
致性是非常重要的東西,如果大家都堅(jiān)持同樣的風(fēng)格,其他人就會(huì)發(fā)現(xiàn)你的代碼很容易讀,
你也容易讀懂其他人的。風(fēng)格的細(xì)節(jié)可以通過一些局部規(guī)定,或管理性的公告,或者通過程
序來處理。如果沒有這類東西,那么最好就是遵循大眾廣泛采納的規(guī)矩。我們?cè)谶@里將遵循
《C程序設(shè)計(jì)語言》(The C Programming Language)一書中所使用的風(fēng)格,在處理J a v a和C++ 程
序時(shí)做一些小的調(diào)整。
我們一般將用一些好的和不好的小程序設(shè)計(jì)例子來說明與風(fēng)格有關(guān)的規(guī)則,因?yàn)閷?duì)處理
同樣事物的兩種方式做比較常常很有啟發(fā)性。這些例子不是人為臆造的,不好的一個(gè)都來自
實(shí)際代碼,由那些在太多工作負(fù)擔(dān)和太少時(shí)間的壓力下工作的普通程序員(偶然就是我們自己)
寫出來。為了簡(jiǎn)單,這里對(duì)有些代碼做了些精練,但并沒有對(duì)它們做任何錯(cuò)誤的解釋。在看
到這些代碼之后,我們將重寫它們,說明如何對(duì)它們做些改進(jìn)。由于這里使用的都是真實(shí)代
碼,所以代碼中可能存在多方面問題。要指出代碼里的所有缺點(diǎn),有時(shí)可能會(huì)使我們遠(yuǎn)離討
論的主題。因此,在有的好代碼例子里也會(huì)遺留下一些未加指明的缺陷。
為了指明一段代碼是不好的,在本書中,我們將在有問題的代碼段的前面標(biāo)出一些問號(hào),
就像下面這段:
? #define ONE 1
? #define TEN 10
? #define TWENTY 20
為什么這些# d e f i n e有問題?請(qǐng)想一想,如果某個(gè)具有T W E N T Y個(gè)元素的數(shù)組需要修改得更大
一點(diǎn),情況將會(huì)怎么樣。至少這里的每個(gè)名字都應(yīng)該換一下,改成能說明這些特殊值在程序
中所起作用的東西。
#def i ne INPUT-MODE 1
#define INPUT-BUFSIZE 10
#def i ne OUTPUT-BUFSIZE 20
1.1 名字
什么是名字?一個(gè)變量或函數(shù)的名字標(biāo)識(shí)這個(gè)對(duì)象,帶著說明其用途的一些信息。一個(gè)名
字應(yīng)該是非形式的、簡(jiǎn)練的、容易記憶的,如果可能的話,最好是能夠拼讀的。許多信息來
自上下文和作用范圍(作用域)。一個(gè)變量的作用域越大,它的名字所攜帶的信息就應(yīng)該越多。
全局變量使用具有說明性的名字,局部變量用短名字。根據(jù)定義,全局變量可以出現(xiàn)在整個(gè)
程序中的任何地方,因此它們的名字應(yīng)該足夠長(zhǎng),具有足夠的說明性,以便使讀者能夠記得
它們是干什么用的。給每個(gè)全局變量聲明附一個(gè)簡(jiǎn)短注釋也非常有幫助:
int npending = 0; // current length of input queue
全局函數(shù)、類和結(jié)構(gòu)也都應(yīng)該有說明性的名字,以表明它們?cè)诔绦蚶锇缪莸慕巧?br>
相反,對(duì)局部變量使用短名字就夠了。在函數(shù)里, n可能就足夠了, n p o i n t s也還可以,
用n u m b e r O f P o i n t s就太過分了。
按常規(guī)方式使用的局部變量可以采用極短的名字。例如用i、j作為循環(huán)變量,p、q作為
指針,s、t表示字符串等。這些東西使用得如此普遍,采用更長(zhǎng)的名字不會(huì)有什么益處或收
獲,可能反而有害。比較:
for (theElementIndex = 0; theElementIndex < number0fElements;
theElementIndex++)
elementArray[theElementIndex] = theElementIndex;
for (i = 0; i < nelems; i++)
elem[i] = i ;
人們常常鼓勵(lì)程序員使用長(zhǎng)的變量名,而不管用在什么地方。這種認(rèn)識(shí)完全是錯(cuò)誤的,清晰
性經(jīng)常是隨著簡(jiǎn)潔而來的。
現(xiàn)實(shí)中存在許多命名約定或者本地習(xí)慣。常見的比如:指針采用以p結(jié)尾的變量名,例如
n o d e p;全局變量用大寫開頭的變量名,例如G l o b a l;常量用完全由大寫字母拼寫的變量
名,如C O N S T A N T S等。有些程序設(shè)計(jì)工場(chǎng)采用的規(guī)則更加徹底,他們要求把變量的類型和用
途等都編排進(jìn)變量名字中。例如用p c h說明這是一個(gè)字符指針,用s t r T o和s t r F r o m表示它
們分別是將要被讀或者被寫的字符串等。至于名字本身的拼寫形式,是使用n p e n d i n g或
n u m P e n d i n g還是n u m _ p e n d i n g,這些不過是個(gè)人的喜好問題,與始終如一地堅(jiān)持一種切
合實(shí)際的約定相比,這些特殊規(guī)矩并不那么重要。
命名約定能使自己的代碼更容易理解,對(duì)別人寫的代碼也是一樣。這些約定也使人在寫
代碼時(shí)更容易決定事物的命名。對(duì)于長(zhǎng)的程序,選擇那些好的、具有說明性的、系統(tǒng)化的名
字就更加重要。
C++ 的名字空間和J a v a的包為管理各種名字的作用域提供了方法,能幫助我們保持名字
的意義清晰,又能避免過長(zhǎng)的名字。
保持一致性。相關(guān)的東西應(yīng)給以相關(guān)的名字,以說明它們的關(guān)系和差異。
除了太長(zhǎng)之外,下面這個(gè)J a v a類中各成員的名字一致性也很差:
class UserQueue {
i n t noOfIternsInQ, frontOiTheQueue,
queuecapacity;
public i n t noOfUsersInQueue() {...
}
這里同一個(gè)詞“隊(duì)列( q u e u e )”在名字里被分別寫為Q、Q u e u e或q u e u e。由于只能在類型
U s e r Q u e u e里訪問,類成員的名字中完全不必提到隊(duì)列,因?yàn)榇嬖谏舷挛?。所以?/p>
queue.queuecapacity
完全是多余的。下面的寫法更好:
class UserQueue {
int ni terns, front, capacity;
public i n t nusers() {. . .}
}
因?yàn)檫@時(shí)可以如此寫:
quue.capacity++;
n = queue.nusers();
這樣做在清晰性方面沒有任何損失。在這里還有可做的事情。例如i t e m s和u s e r s實(shí)際是同一種
東西,同樣?xùn)|西應(yīng)該使用一個(gè)概念。
函數(shù)采用動(dòng)作性的名字。函數(shù)名應(yīng)當(dāng)用動(dòng)作性的動(dòng)詞,后面可以跟著名詞:
now = date .getTirne() ;
putchar('\nl) ;
對(duì)返回布爾類型值(真或者假)的函數(shù)命名,應(yīng)該清楚地反映其返回值情況。下面這樣的語句
if(checkoctal(c)) ...
是不好的,因?yàn)樗鼪]有指明什么時(shí)候返回真,什么時(shí)候返回假。而:
i f (i soctal (c)) . . .
就把事情說清楚了:如果參數(shù)是八進(jìn)制數(shù)字則返回真,否則為假。
要準(zhǔn)確。名字不僅是個(gè)標(biāo)記,它還攜帶著給讀程序人的信息。誤用的名字可能引起奇怪的程
序錯(cuò)誤。
本書作者之一寫過一個(gè)名為i s o c t a l的宏,并且發(fā)布使用多年,而實(shí)際上它的實(shí)現(xiàn)是錯(cuò)誤的:
#define isoctal(c) ((c) >= '0' && (c) <= '8')
正確的應(yīng)該是:
#define isoctal(c) ((c) >= '0' && (c) <= '7')
這是另外一種情況:名字具有正確的含義,而對(duì)應(yīng)的實(shí)現(xiàn)卻是錯(cuò)的,一個(gè)合情合理的名字掩
蓋了一個(gè)害人的實(shí)現(xiàn)。
下面是另一個(gè)例子,其中的名字和實(shí)現(xiàn)完全是矛盾的:
public boolean inTable(0bject obj) {
i n t j = t h i s .getIndex(obj) ;
return (j == nTable);
}
函數(shù)g e t I n d e x如果找到了有關(guān)對(duì)象,就返回0到n T a b l e-1之間的一個(gè)值;否則返回
n T a b l e值。而這里i n T a b l e返回的布爾值卻正好與它名字所說的相反。在寫這段代碼時(shí),
這種寫法未必會(huì)引起什么問題。但如果后來修改這個(gè)程序,很可能是由別的程序員來做,這
個(gè)名字肯定會(huì)把人弄糊涂。