人们看到最好的作家有时并不理会修辞学的规则。还好,当他们这样做虽然付出了违反常规的代价,读者还经常能从句子中发现某些具有补偿性的价值。除非作者自己也明确其做法的意思,否则最好还是按规矩做。
William Strunk 和E. B. White,《风格的要素》
下面这段代码取自一个许多年前写的大程序:

这段代码写得很仔细,具有很好的格式。它所在的程序也工作得很好。写这个系统的程序员会对他们的工作感到骄傲。但是这段摘录却会把细心的读者搞糊涂:新加坡、文莱、波兰和意大利之间有什么关系?为什么在注释里没有提到意大利?由于注释与代码不同,其中必然有一个有错,也可能两个都不对。这段代码经过了执行和测试,所以它可能没有问题。注释中对提到的三个国家间的关系没有讲清楚,如果你要维护这些代码,就必须知道更多的东西。
上面这几行实际代码是非常典型的:大致上写得不错,但也还存在许多应该改进的地方。
本书关心的是程序设计实践,关心怎样写出实际的程序。我们的目的是帮助读者写出这样的软件,它至少像上面的代码所在的程序那样工作得非常好,而同时又能避免那些污点和弱点。我们将讨论如何从一开始就写出更好的代码,以及如何在代码的发展过程中进一步改进它。
我们将从一个很平凡的地方入手,首先讨论程序设计的风格问题。风格的作用主要就是使代码容易读,无论是对程序员本人,还是对其他人。好的风格对于好的程序设计具有关键性作用。我们希望最先谈论风格,也是为了使读者在阅读本书其余部分时能特别注意这个问题。
写好一个程序,当然需要使它符合语法规则、修正其中的错误和使它运行得足够快,但是实际应该做的远比这多得多。程序不仅需要给计算机读,也要给程序员读。一个写得好的程序比那些写得差的程序更容易读、更容易修改。经过了如何写好程序的训练,生产的代码更可能是正确的。幸运的是,这种训练并不太困难。
程序设计风格的原则根源于由实际经验中得到的常识,它不是随意的规则或者处方。代码应该是清楚的和简单的—具有直截了当的逻辑、自然的表达式、通行的语言使用方式、有意义的名字和有帮助作用的注释等,应该避免耍小聪明的花招,不使用非正规的结构。一致性是非常重要的东西,如果大家都坚持同样的风格,其他人就会发现你的代码很容易读,你也容易读懂其他人的。风格的细节可以通过一些局部规定,或管理性的公告,或者通过程序来处理。如果没有这类东西,那么最好就是遵循大众广泛采纳的规矩。我们在这里将遵循《C程序设计语言》(The C Programming Language)一书中所使用的风格,在处理Java和C++ 程序时做一些小的调整。
我们一般将用一些好的和不好的小程序设计例子来说明与风格有关的规则,因为对处理同样事物的两种方式做比较常常很有启发性。这些例子不是人为臆造的,不好的一个都来自实际代码,由那些在太多工作负担和太少时间的压力下工作的普通程序员(偶然就是我们自己)写出来。为了简单,这里对有些代码做了些精练,但并没有对它们做任何错误的解释。在看到这些代码之后,我们将重写它们,说明如何对它们做些改进。由于这里使用的都是真实代码,所以代码中可能存在多方面问题。要指出代码里的所有缺点,有时可能会使我们远离讨论的主题。因此,在有的好代码例子里也会遗留下一些未加指明的缺陷。
为了指明一段代码是不好的,在本书中,我们将在有问题的代码段的前面标出一些问号,就像下面这段:
![]()
为什么这些#define有问题?请想一想,如果某个具有TWENTY个元素的数组需要修改得更大一点,情况将会怎么样。至少这里的每个名字都应该换一下,改成能说明这些特殊值在程序中所起作用的东西。
![]()
1.1 名字
什么是名字?一个变量或函数的名字标识这个对象,带着说明其用途的一些信息。名字应该是非形式的、简练的、容易记忆的,如果可能的话,最好是能够拼读的。许多信息来自上下文和作用范围(作用域)。变量的作用域越大,它的名字所携带的信息就应该越多。
全局变量使用具有说明性的名字,局部变量用短名字。根据定义,全局变量可以出现在整个程序中的任何地方,因此它们的名字应该足够长,具有足够的说明性,以便使读者能够记得它们是干什么用的。给每个全局变量声明附一个简短注释也非常有帮助:
![]()
全局函数、类和结构也都应该有说明性的名字,以表明它们在程序里扮演的角色。
相反,对局部变量使用短名字就够了。在函数里,n可能就足够了,npoints也还可以,用numberOfPoints就太过分了。
按常规方式使用的局部变量可以采用极短的名字。例如用i、j作为循环变量,p、q作为指针,s、t表示字符串等。这些东西使用得如此普遍,采用更长的名字不会有什么益处或收获,可能反而有害。比较:和
![]()
![]()
![]()
人们常常鼓励程序员使用长的变量名,而不管用在什么地方。这种认识完全是错误的,清晰性经常是随着简洁而来的。
现实中存在许多命名约定或者本地习惯。常见的比如:指针采用以p结尾的变量名,例如nodep;全局变量用大写开头的变量名,例如Global;常量用完全由大写字母拼写的变量名,如CONSTANTS等。有些程序设计工场采用的规则更加彻底,他们要求把变量的类型和用途等都编排进变量名字中。例如用pch说明这是一个字符指针,用strTo和strFrom表示它们分别是将要被读或者被写的字符串等。至于名字本身的拼写形式,是使用npending或numPending还是num_pending,这些不过是个人的喜好问题,与始终如一地坚持一种切合实际的约定相比,这些特殊规矩并不那么重要。
命名约定能使自己的代码更容易理解,对别人写的代码也是一样。这些约定也使人在写代码时更容易决定事物的命名。对于长的程序,选择那些好的、具有说明性的、系统化的名字就更加重要。
C++的名字空间和Java的包为管理各种名字的作用域提供了方法,能帮助我们保持名字的意义清晰,又能避免过长的名字。保持一致性。相关的东西应给以相关的名字,以说明它们的关系和差异。
除了太长之外,下面这个Java类中各成员的名字一致性也很差:

这里同一个词“队列(queue)”在名字里被分别写为Q、Queue或queue。由于只能在类型UserQueue里访问,类成员的名字中完全不必提到队列,因为存在上下文。所以:
![]()
完全是多余的。下面的写法更好:

因为这时可以如此写:
![]()
这样做在清晰性方面没有任何损失。在这里还有可做的事情。例如items和users实际是同一种东西,同样东西应该使用一个概念。函数采用动作性的名字。函数名应当用动作性的动词,后面可以跟着名词:
![]()
对返回布尔类型值(真或者假)的函数命名,应该清楚地反映其返回值情况。下面这样的语句
![]()
是不好的,因为它没有指明什么时候返回真,什么时候返回假。而:
![]()
就把事情说清楚了:如果参数是八进制数字则返回真,否则为假。要准确。名字不仅是个标记,它还携带着给读程序人的信息。误用的名字可能引起奇怪的程序错误。
本书作者之一写过一个名为isoctal的宏,并且发布使用多年,而实际上它的实现是错误的:
![]()
正确的应该是:
![]()
这是另外一种情况:名字具有正确的含义,而对应的实现却是错的,一个合情合理的名字掩盖了一个害人的实现。
下面是另一个例子,其中的名字和实现完全是矛盾的:

函数getIndex如果找到了有关对象,就返回0到nTable-1之间的一个值;否则返回nTable 值。而这里inTable返回的布尔值却正好与它名字所说的相反。在写这段代码时,这种写法未必会引起什么问题。但如果后来修改这个程序,很可能是由别的程序员来做,这个名字肯定会把人弄糊涂。
练习1-1 评论下面代码中名字和值的选择:

练习1-2 改进下面的函数:

练习1-3 大声读出下面的代码:
![]()







