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