人们看到最好的作家有时并不理会修辞学的规则。还好,当他们这样做虽然付
出了违反常规的代价,读者还经常能从句子中发现某些具有补偿性的价值。除非作
者自己也明确其做法的意思,否则最好还是按规矩做。
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返回的布尔值却正好与它名字所说的相反。在写这段代码时,
这种写法未必会引起什么问题。但如果后来修改这个程序,很可能是由别的程序员来做,这
个名字肯定会把人弄糊涂。