f o r ( N o d e * n o d e = l i s t - > h e a d ; n o d e ! = N U L L ; n o d e = n o d e - > n e x t )
P r i n t ( n o d e - > d a t a )
r e t u r n e x p o n e n t > = 0 ? m a n t i s s a * ( 1 < < e x p o n e n t ) : m a n t i s s a / ( 1 < < - e x p o n e n t ) ;
它比下面这段要好些还是差些?
i f ( e x p o n e n t > = 0 ) {
r e t u r n m a n t i s s a * ( 1 < < e x p o n e n t ) ;
} e l s e {
r e t u r n m a n t i s s a / ( 1 < < - e x p o n e n t ) ;
}
但少的代码并不总是更好!很多时候,像下面这样的一行表达式:
a s s e r t ( ( ! ( b u c k e t = F i n d B u c k e t ( k e y ) ) ) | | ! b u c k e t - > I s O c c u p i e d ( ) ) ;
理解起来要比两行代码花更多时间:
b u c k e t = F i n d B u c k e t ( k e y ) ;
i f ( b u c k e t ! = N U L L ) a s s e r t ( ! b u c k e t - > I s O c c u p i e d ( ) ) ;
把信息装到名字里
无论是命名变量、函数还是类,都可以使用很多相同的原则。
我们在程序中见到的很多名字都很模糊,例如t m p。就算是看上去合理的词,如s i z e或者
get,也都没有装入很多信息。本章会告诉你如何把信息装入名字中。
本章分成6 个专题:
选择专业的词。 •
避免泛泛的名字(或者说要知道什么时候使用它)。 •
用具体的名字代替抽象的名字。 •
使用前缀或后缀来给名字附带更多信息。 •
决定名字的长度。 •
利用名字的格式来表达含义
选择专业的词
“把信息装入名字中”包括要选择非常专业的词,并且避免使用“空洞”的词。
例如,“get”这个词就非常不专业,例如在下面的例子中:
d e f G e t P a g e ( u r l ) : . . .
“g e t”这个词没有表达出很多信息。这个方法是从本地的缓存中得到一个页面,还是从
数据库中,或者从互联网中?如果是从互联网中,更专业的名字可以是F e t c h P a g e()或者
DownloadPage()。
下面是一个BinaryTree类的例子:
c l a s s B i n a r y T r e e
{ i n t S i z e ( ) ;
. . .
} ;
你期望Size()方法返回什么呢?树的高度,节点数,还是树在内存中所占的空间?
问题是S i z e ( ) 没有承载很多信息。更专业的词可以是H e i g h t ( ) 、N u m N o d e s ( ) 或者
MemoryBytes()。
找到更有表现力的词
要勇于使用同义词典或者问朋友更好的名字建议。英语是一门丰富的语言,有很多词可
以选择。
下面是一些例子,这些单词更有表现力,可能适合你的语境:
单词 更多选择
send deliver、 dispatch、announce、distribute、route
find search、extract、locate、recover
start launch、create、begin、open
make create、set up、build、generate、compose、add、new
避免像tmp 和retval 这样泛泛的名字
使用像t m p、r e t v a l和f o o这样的名字往往是“我想不出名字”的托辞。与其使用这样空
洞的名字,不如挑一个能描述这个实体的值或者目的的名字。
例如,下面的J a v a S c r i p t 函数使用了retval:
v a r e u c l i d e a n _ n o r m = f u n c t i o n ( v ) {
v a r r e t v a l = 0 . 0 ;
f o r ( v a r i = 0 ; i < v . l e n g t h ; i + = 1 )
r e t v a l + = v [ i ] * v [ i ] ;
r e t u r n M a t h . s q r t ( r e t v a l ) ;
} ;
当你想不出更好的名字来命名返回值时,很容易想到使用r e t v a l。但r e t v a l除了“我是
一个返回值”外并没有包含更多信息(这里的意义往往也是很明显的)。
好的名字应当描述变量的目的或者它所承载的值。在本例中,这个变量正在累加v 的平
方。因此更贴切的名字可以是s u m_s q u a r e s。这样就提前声明了这个变量的目的,并且可
能会帮忙找到缺陷。
好的名字应当描述变量的目的或者它所承载的值。在本例中,这个变量正在累加v 的平
方。因此更贴切的名字可以是s u m_s q u a r e s。这样就提前声明了这个变量的目的,并且可
能会帮忙找到缺陷。
建议
r e t v a l 这个名字没有包含很多信息。用一个描述该变量的值的名字来代替它。
tmp
请想象一下交换两个变量的经典情形:
i f ( r i g h t < l e f t ) {
t m p = r i g h t ;
r i g h t = l e f t ;
l e f t = t m p ;
}
在这种情况下,t m p 这个名字很好。这个变量唯一的目的就是临时存储,它的整个生命周
期只在几行代码之间。t m p 这个名字向读者传递特定信息,也就是这个变量没有其他职
责,它不会被传到其他函数中或者被重置以反复使用。
但在下面的例子中对tmp的使用仅仅是因为懒惰:
S t r i n g t m p = u s e r . n a m e ( ) ;
t m p + = " " + u s e r . p h o n e _ n u m b e r ( ) ;
t m p + = " " + u s e r . e m a i l ( ) ;
. . .
t e m p l a t e . s e t ( " u s e r _ i n f o " , t m p ) ;
尽管这里的变量只有很短的生命周期,但对它来讲最重要的并不是临时存储。用像
user_info这样的名字来代替可能会更具描述性。
但在下面的例子中对tmp的使用仅仅是因为懒惰:
S t r i n g t m p = u s e r . n a m e ( ) ;
t m p + = " " + u s e r . p h o n e _ n u m b e r ( ) ;
t m p + = " " + u s e r . e m a i l ( ) ;
. . .
t e m p l a t e . s e t ( " u s e r _ i n f o " , t m p ) ;
尽管这里的变量只有很短的生命周期,但对它来讲最重要的并不是临时存储。用像
user_info这样的名字来代替可能会更具描述性。
在下面的情况中,tmp应当出现在名字中,但只是名字的一部分:
t m p _ fi l e = t e m p fi l e . N a m e d T e m p o r a r y F i l e ( )
. . .
S a v e D a t a ( t m p _ fi l e , . . . )
请注意我们把变量命名为tmp_file而非只是tmp,因为这是一个文件对象。想象一下如果我
们只是把它叫做tmp:
S a v e D a t a ( t m p , . . . )
只要看看这么一行代码,就会发现不清楚tmp到底是文件、文件名还是要写入的数据。
建议
tmp这个名字只应用于短期存在且临时性为其主要存在因素的变量。
循环迭代器
像i 、j 、i t e r 和i t 等名字常用做索引和循环迭代器。尽管这些名字很空泛,但是大家都
知道它们的意思是“我是一个迭代器”(实际上,如果你用这些名字来表示其他含义,
那会很混乱。所以不要这么做!)
但有时会有比i 、j 、k 更贴切的迭代器命名。例如,下面的循环要找到哪个u s e r 属于哪个
club:
f o r ( i n t i = 0 ; i < c l u b s . s i z e ( ) ; i + + )
f o r ( i n t j = 0 ; j < c l u b s [ i ] . m e m b e r s . s i z e ( ) ; j + + )
f o r ( i n t k = 0 ; k < u s e r s . s i z e ( ) ; k + + )
16 第2章
i f ( c l u b s [ i ] . m e m b e r s [ k ] = = u s e r s [ j ] )
c o u t < < " u s e r [ " < < j < < " ] i s i n c l u b [ " < < i < < " ] " < < e n d l ;
在i f条件语句中,m e m b e r s[]和u s e r s[]用了错误的索引。这样的缺陷很难发现,因为这
一行代码单独来看似乎没什么问题:
i f ( c l u b s [ i ] . m e m b e r s [ k ] = = u s e r s [ j ] )
在这种情况下,使用更精确的名字可能会有帮助。如果不把循环索引命名为(i 、j 、
k),另一个选择可以是(c l u b_i、m e m b e r s_i、u s e r_i)或者,更简化一点(c i、m i、
ui)。这种方式会帮助把代码中的缺陷变得更明显:
i f ( c l u b s [ c i ] . m e m b e r s [ u i ] = = u s e r s [ m i ] ) # 缺陷!第一个字母不匹配。
如果用得正确,索引的第一个字母应该与数据的第一个字符匹配:
i f ( c l u b s [ c i ] . m e m b e r s [ m i ] = = u s e r s [ u i ] ) # O K 。首字母匹配
对于空泛名字的裁定
如你所见,在某些情况下空泛的名字也有用处。
建议
如果你要使用像t m p 、i t 或者r e t v a l 这样空泛的名字,那么你要有个好的理由。
很多时候,仅仅因为懒惰而滥用它们。这可以理解,如果想不出更好的名字,那么用个
没有意义的名字,像f o o ,然后继续做别的事,这很容易。但如果你养成习惯多花几秒
钟想出个好名字,你会发现你的“命名能力”很快提升。
用具体的名字代替抽象的名字
在给变量、函数或者其他元素命名时,要把它描述得更具体而不是更抽象。
例如,假设你有一个内部方法叫做S e r v e r C a n S t a r t(),它检测服务是否可以监听某个给
定的T C P / I P 端口。然而S e r v e r C a n S t a r t()有点抽象。C a n L i s t e n O n P o r t()就更具体一些。
这个名字直接地描述了这个方法要做什么事情。
例子: DISALLOW_EVIL_CONSTRUCTORS
这个例子来自G o o g l e 的代码库。在C + + 里,如果你不为类定义拷贝构造函数或者赋值操
作符,那就会有一个默认的。尽管这很方便,这些方法很容易导致内存泄漏以及其他灾
难,因为它们在你可能想不到的“幕后”地方运行。
所以,G o o g l e 有个便利的方法来禁止这些“邪恶”的建构函数,就是用这个宏:
c l a s s C l a s s N a m e {
18 第2章
p r i v a t e :
D I S A L L O W _ E V I L _ C O N S T R U C T O R S ( ClassName ) ;
p u b l i c : . . .
} ;
这个宏定义成:
# d e fi n e D I S A L L O W _ E V I L _ C O N S T R U C T O R S ( C l a s s N a m e ) \
C l a s s N a m e ( c o n s t C l a s s N a m e & ) ; \
v o i d o p e r a t o r = ( c o n s t C l a s s N a m e & ) ;
通过把这个宏放在类的私有部分中,这两个方法
译注1
成为私有的,所以不能用它们,即
使意料之外的使用也是不可能的。
然而D I S A L L O W_E V I L_C O N S T R U C T O R S这个名字并不是很好。对于“邪恶”这个词的使用包
含了对于一个有争议话题过于强烈的立场。更重要的是,这个宏到底禁止了什么这一点
是不清楚的。它禁止了operator=()方法,但这个方法甚至根本就不是构造函数!
这个名字使用了几年,但最终换成了一个不那么嚣张而且更具体的名字:
# d e fi n e D I S A L L O W _ C O P Y _ A N D _ A S S I G N ( C l a s s N a m e ) . . .
为名字附带更多信息
一个变量名就像是一个小小的注释。尽管空间不是很大,但不管你在名
中挤进任何额外的信息,每次有人看到这个变量名时都会同时看到这些信息。
例如,假设你有一个变量包含一个十六进制字符串:
s t r i n g i d ; / / E x a m p l e : " a f 8 4 e f 8 4 5 c d 8 "
如果让读者记住这个I D 的格式很重要的话,你可以把它改名为hex_id。
带单位的值
如果你的变量是一个度量的话(如时间长度或者字节数),那么最好把名字带上它的
单位。
例如,这里有些J a v a S c r i p t 代码用来度量一个网页的加载时间:
v a r s t a r t = ( n e w D a t e ( ) ) . g e t T i m e ( ) ; / / t o p o f t h e p a g e
. . .
v a r e l a p s e d = ( n e w D a t e ( ) ) . g e t T i m e ( ) - s t a r t ; / / b o t t o m o f t h e p a g e
d o c u m e n t . w r i t e l n ( " L o a d t i m e w a s : " + e l a p s e d + " s e c o n d s " ) ;
这段代码里没有明显的错误,但它不能正常运行,因为getTime()会返回毫秒而非秒。
通过给变量结尾追加_ms,我们可以让所有的地方更明确:
v a r s t a r t _ m s = ( n e w D a t e ( ) ) . g e t T i m e ( ) ; / / t o p o f t h e p a g e
. . .
v a r e l a p s e d _ m s = ( n e w D a t e ( ) ) . g e t T i m e ( ) - s t a r t _ m s ; / / b o t t o m o f t h e p a g e
d o c u m e n t . w r i t e l n ( " L o a d t i m e w a s : " + e l a p s e d _ m s / 1 0 0 0 + " s e c o n d s " ) ;
除了时间,还有很多在编程时会遇到的单位。下表列出一些没有单位的函数参数以及带
单位的版本:
函数参数 带单位的参数
Start(int d e l a y ) delay → d e l a y _ s e c s
CreateCache(int s i z e ) size → s i z e _ m b
ThrottleDownload(float l i m i t ) limit → m a x _ k b p s
Rotate(float a n g l e ) angle → d e g r e e s _ c w
附带其他重要属性
情形 变量名 更好的名字
一个“纯文本”格式的密码,需要加密后才能 password p l a i n t e x t _password
进一步使用
一条用户提供的注释,需要转义之后才能用于显示 comment u n e s c a p e d _comment
已转化为U T F - 8 格式的h t m l 字节 html html_u t f 8
以“u r l 方式编码”的输入数据 data data_u r l e n c
但你不应该给程序中每个变量都加上像u n e s c a p e d_或者_ut f8这样的属性。如果有人误解
了这个变量就很容易产生缺陷,尤其是会产生像安全缺陷这样可怕的结果,在这些地方
这种技巧最有用武之地。基本上,如果这是一个需要理解的关键信息,那就把它放在名
字里。
匈牙利表示法是一个在微软广泛应用的命名系统,它把每个变量的“类型”
信息都编写进名字的前缀里。下面有几个例子:
名字 含义
pLast 指向某数据结构最后一个元素的指针(p )
pszBuffer 指向一个以零结尾(z )的字符串(s )的指针(p )
cch 一个字符(c h )计数(c )
mpcopx 在指向颜色的指针(p c o )和指向x 轴长度的指针(p x )之间的一
个映射(m )
这实际上就是“给名字附带上属性”的例子。但它是一种更正式和严格的系
统,关注于特有的一系列属性。
我们在这一部分所提倡的是更广泛的、更加非正式的系统:标识变量的任何
关键属性,如果需要的话以易读的方式把它加到名字里。你可以把这称为
“英语表示法”。
名字应该有多长
在小的作用域里可以使用短的名字
当你去短期度假时,你带的行李通常会比长假少。同样,“作用域”小的标识符(对于
多少行其他代码可见)也不用带上太多信息。也就是说,因为所有的信息(变量的类
型、它的初值、如何析构等)都很容易看到,所以可以用很短的名字。
i f ( d e b u g ) {
m a p < s t r i n g , i n t > m ;
L o o k U p N a m e s N u m b e r s ( & m ) ;
P r i n t ( m ) ;
}
尽管m 这个名字并没有包含很多信息,但这不是个问题。因为读者已经有了需要理解这
段代码的所有信息。
然而,假设m是一个全局变量中的类成员,如果你看到这个代码片段:
L o o k U p N a m e s N u m b e r s ( & m ) ;
P r i n t ( m ) ;
这段代码就没有那么好读了,因为m的类型和目的都不明确。
因此如果一个标识符有较大的作用域,那么它的名字就要包含足够的信息以便含义更
清楚。
输入长名字——不再是个问题
有很多避免使用长名字的理由,但“不好输入”这一条已经不再有效。我们所见到的所
有的编程文本编辑器都有内置的“单词补全”的功能。令人惊讶的是,大多数程序员并
没有注意到这个功能。如果你还没在你的编辑器上试过这个功能,那么请现在就放下本
书然后试一下下面这些功能:
1 . 键入名字的前面几个字符。
2 . 触发单词补全功能(见下表)。
3 . 如果补全的单词不正确,一直触发这个功能直到正确的名字出现 。
它非常准确。这个功能在任何语种的任何类型的文件中都可以用。并且它对于任何单词
(t o k e n )都有效,甚至在你输入注释时也行。
编辑器 命令
Vi Ctrl+p
Emacs Meta+/(先按E S C ,然后按/ )
Eclipse Alt+/
IntelliJ IDEA Alt+/
TextMate ESC
首字母缩略词和缩写
程序员有时会采用首字母缩略词和缩写来命令,以便保持较短的名字,例如,把一个类
命名为BEManager而不是BackEndManager。这种名字会让人费解,冒这种风险是否值得?
在我们的经验中,使用项目所特有的缩写词非常糟糕。对于项目的新成员来讲它们看上
去太令人费解和陌生,当过了相当长的时间以后,即使是对于原作者来讲,它们也会变
得令人费解和陌生。
所以经验原则是:团队的新成员是否能理解这个名字的含义?如果能,那可能就没有
问题。
例如,对程序员来讲,使用e v a l来代替e v a l u a t i o n,用d o c来代替d o c u m e n t,用s t r来代
替s t r i n g是相当普遍的。因此如果团队的新成员看到F o r m a t S t r()可能会理解它是什么意
思,然而,理解BEManager可能有点困难。
丢掉没用的词
有时名字中的某些单词可以拿掉而不会损失任何信息。例如,C o n v e r t T o S t r i n g ( )
就不如T o S t r i n g ( ) 这个更短的名字,而且没有丢失任何有用的信息。同样,不用
DoServeLoop(),ServeLoop()也一样清楚。
利用名字的格式来传递含义
对于下划线、连字符和大小写的使用方式也可以把更多信息装到名字中。例如,下面是
一些遵循G o o g l e 开源项目格式规范的C + + 代码:
s t a t i c c o n s t i n t k M a x O p e n F i l e s = 1 0 0 ;
c l a s s L o g R e a d e r {
p u b l i c :
v o i d O p e n F i l e ( s t r i n g l o c a l _ fi l e ) ;
p r i v a t e :
i n t o f f s e t _ ;
D I S A L L O W _ C O P Y _ A N D _ A S S I G N ( L o g R e a d e r ) ;
} ;
对不同的实体使用不同的格式就像语法高亮显示的形式一样,能帮你更容易地阅读
代码。
该例子中的大部分格式都很常见,使用C a m e l C a s e来表示类名,使用l o w e r_s e p a r a t e d来
表示变量名。但有些规范也可能会出乎你的意料。
例如,常量的格式是k C o n s t a n t N a m e而不是C O N S T A N T_N A M E。这种形式的好处是容易和
#define的宏区分开,宏的规范是MACRO_NAME。
类成员变量和普通变量一样,但必须以一条下划线结尾,如o f f s e t _ 。刚开始看,可能会
觉得这个规范有点怪,但是能立刻区分出是成员变量还是其他变量,这一点还是很方便
的。例如,如果你在浏览一个大的方法中的代码,看到这样一行:
s t a t s . c l e a r ( ) ;
你本来可能要想“s t a t s属于这个类吗?这行代码是否会改变这个类的内部状态?”如果
用了m e m b e r_这个规范,你就能迅速得到结论:“不,s t a t s一定是个局部变量。否则它就
会命名为stats_。”
其他格式规范
根据项目上下文或语言的不同,还可以采用其他一些格式规范使得名字包含更多信息。
例如,在《J a v a S c r i p t :T h e G o o d P a r t s 》( D o u g l a s C r o c k f o r d , O ’ R e i l l y, 2 0 0 8 )一书中,
作者建议“构造函数”(在新建时会调用的函数)应该首字母大写而普通函数首字母
小字:
v a r x = n e w D a t e P i c k e r ( ) ; / / D a t e P i c k e r ( ) i s a " c o n s t r u c t o r " f u n c t i o n
v a r y = p a g e H e i g h t ( ) ; / / p a g e H e i g h t ( ) i s a n o r d i n a r y f u n c t i o n
下面是另一个J a v a S c r i p t 例子:当调用j Q u e r y 库函数时(它的名字是单个字符$),一条
非常有用的规范是,给j Q u e r y 返回的结果也加上$ 作为前缀:
v a r $ a l l _ i m a g e s = $ ( " i m g " ) ; / / $ a l l _ i m a g e s i s a j Q u e r y o b j e c t
v a r h e i g h t = 2 5 0 ; / / h e i g h t i s n o t
在整段代码中,都会清楚地看到$all_images是个j Q u e r y 返回对象。
下面是最后一个例子,这次是H T M L / C S S :当给一个H T M L 标记加i d或者c l a s s属性时,
下划线和连字符都是合法的值。一个可能的规范是用下划线来分开I D 中的单词,用连字
符来分开class中的单词。
< d i v i d = " m i d d l e _ c o l u m n " c l a s s = " m a i n - c o n t e n t " > . . .
是否要采用这些规范是由你和你的团队决定的。但不论你用哪个系统,在你的项目中要
保持一致。
总结
本章唯一的主题是:把信息塞入名字中。这句话的含意是,读者仅通过读到名字就可以
获得大量信息。
下面是讨论过的几个小提示:
使用专业的单词 • ——例如,不用G e t,而用F e t c h或者D o w n l o a d可能会更好,这由上
下文决定。
避免空泛的名字 • ,像tmp和retval,除非使用它们有特殊的理由。
•
使用具体的名字来更细致地描述事物——S e r v e r C a n S t a r t ( ) 这个名字就比
CanListenOnPort更不清楚。
给变量名带上重要的细节 • ——例如,在值为毫秒的变量后面加上_ m s ,或者在还需要
转义的,未处理的变量前面加上raw_。
为作用域大的名字采用更长的名字 • ——不要用让人费解的一个或两个字母的名字来
命名在几屏之间都可见的变量。对于只存在于几行之间的变量用短一点的名字更
好。
有目的地使用大小写、下划线等 • ——例如,你可以在类成员和局部变量后面加上
"_"来区分它们。
推荐用min和max 来表示(包含)极限
假设你的购物车应用程序最多不能超过1 0 件物品:
C A R T _ T O O _ B I G _ L I M I T = 1 0
i f s h o p p i n g _ c a r t . n u m _ i t e m s ( ) > = C A R T _ T O O _ B I G _ L I M I T :
E r r o r ( " T o o m a n y i t e m s i n c a r t . " )
这段代码有个经典的“大小差一”缺陷。我们可以简单地通过把>=变成>来改正它:
i f s h o p p i n g _ c a r t . n u m _ i t e m s ( ) > C A R T _ T O O _ B I G _ L I M I T :
(或者通过把C A R T_T O O_B I G_L I M I T变成11)。但问题的根源在于 C A R T_T O O_B I G_L I M I T是
个二义性名字,它的含义到底是“少于”还是“少于/ 且包括”。
建议
命名极限最清楚的方式是在要限制的东西前加上m a x _ 或者m i n _ 。
在本例中,名字应当是MAX_ITEMS_IN_CART,新代码现在变得简单又清楚:
M A X _ I T E M S _ I N _ C A R T = 1 0
i f s h o p p i n g _ c a r t . n u m _ i t e m s ( ) > M A X _ I T E M S _ I N _ C A R T :
E r r o r ( " T o o m a n y i t e m s i n c a r t . " )
推荐用first和last 来表示包含的范围
下面是另一个例子,你没法判断它是“少于”还是“少于且包含”:
p r i n t i n t e g e r _ r a n g e ( s t a r t = 2 , s t o p = 4 )
# D o e s t h i s p r i n t [ 2 , 3 ] o r [ 2 , 3 , 4 ] ( o r s o m e t h i n g e l s e ) ?
尽管s t a r t是个合理的参数名,但s t o p可以有多种解读。对于这样包含的范围(这种范围
包含开头和结尾),一个好的选择是first/last。例如:
s e t . P r i n t K e y s ( fi r s t = " B a r t " , l a s t = " M a g g i e " )
不像stop,last这个名字明显是包含的。
除了f i r s t/l a s t,m i n/m a x这两个名字也适用于包含的范围,如果它们在上下文中“听上
去合理”的话。
推荐用begin和end 来表示包含/排除范围
a b c d
begin end
在实践中,很多时候用包含/ 排除范围更方便。例如,如果你想打印所有发生在1 0 月1 6 日
的事件,那么写成这样很简单:
P r i n t E v e n t s I n R a n g e ( " O C T 1 6 1 2 : 0 0 a m " , " O C T 1 7 1 2 : 0 0 a m " )
这样写就没那么简单了:
P r i n t E v e n t s I n R a n g e ( " O C T 1 6 1 2 : 0 0 a m " , " O C T 1 6 1 1 : 5 9 : 5 9 . 9 9 9 9 p m " )
因此对于这些参数来讲,什么样的一对名字更好呢?对于命名包含/ 排除范围典型的编程
规范是使用begin/end。
但是e n d这个词有点二义性。例如,在句子“我读到这本书的e n d 部分了”,这里的e n d 是
包含的。遗憾的是,英语中没有一个合适的词来表示“刚好超过最后一个值”。
因为对b e g i n/e n d的使用是如此常见(至少在C + + 标准库中是这样用的,还有大多数需要
“分片”的数组也是这样用的),它已经是最好的选择了。
给布尔值命名
当为布尔变量或者返回布尔值的函数选择名字时,要确保返回t r u e 和f a l s e 的意义很
明确。
下面是个危险的例子:
b o o l r e a d _ p a s s w o r d = t r u e ;
这会有两种截然不同的解释:
我们需要读取密码。 •
已经读取了密码。 •
在本例中,最好避免用“r e a d ”这个词,用n e e d_p a s s w o r d或者u s e r_i s_a u t h e n t i c a t e d这
样的名字来代替。
通常来讲,加上像is、has、can或should这样的词,可以把布尔值变得更明确。
例如,S p a c e L e f t()函数听上去像是会返回一个数字,如果它的本意是返回一个布尔值,
可能HasSapceLeft()个这名字更好一些。
最后,最好避免使用反义名字。例如,不要用:
b o o l d i s a b l e _ s s l = f a l s e ;
而更简单易读(而且更紧凑)的表示方式是:
b o o l u s e _ s s l = t r u e ;
与使用者的期望相匹配
有些名字之所以会让人误解是因为用户对它们的含义有先入为主的印象,就算你的本意
并非如此。在这种情况下,最好放弃这个名字而改用一个不会让人误解的名字。
例子:get*()
很多程序员都习惯了把以g e t 开始的方法当做“轻量级访问器”这样的用法,它只是简
单地返回一个内部成员变量。如果违背这个习惯很可能会误导用户。
以下是一个用J a v a 写的例子,请不要这样做:
p u b l i c c l a s s S t a t i s t i c s C o l l e c t o r {
p u b l i c v o i d a d d S a m p l e ( d o u b l e x ) { . . . }
p u b l i c d o u b l e g e t M e a n ( ) {
/ / I t e r a t e t h r o u g h a l l s a m p l e s a n d r e t u r n t o t a l / n u m _ s a m p l e s
}
. . .
}
在这个例子中,g e t M e a n()的实现是要遍历所有经过的数据并同时计算中值。如果有大量
的数据的话,这样的一步可能会有很大的代价!但一个容易轻信的程序员可能会随意地
调用getMean(),还以为这是个没什么代价的调用。
相反,这个方法应当重命名为像c o m p u t e M e a n()这样的名字,后者听起来更像是有些代价
的操作。(另一种做法是,用新的实现方法使它真的成为一个轻量级的操作。)
例子:list::size()
下面是一个来自C + + 标准库中的例子。曾经有个很难发现的缺陷,使得我们的一台服务
器慢得像蜗牛在爬,就是下面的代码造成的:
v o i d S h r i n k L i s t ( l i s t < N o d e > & l i s t , i n t m a x _ s i z e ) {
w h i l e ( l i s t . s i z e ( ) > m a x _ s i z e ) {
F r e e N o d e ( l i s t . b a c k ( ) ) ;
l i s t . p o p _ b a c k ( ) ;
}
}
这里的“缺陷”是,作者不知道l i s t.s i z e()是一个O(n)操作——它要一个节点一个节点
地历数列表,而不是只返回一个事先算好的个数,这就使得S h r i n k L i s t()成了一个O(n
2
)
操作。
这段代码从技术上来讲“正确”,事实上它也通过了所有的单元测试。但当把
ShrinkList()应用于有1 0 0 万个元素的列表上时,要花超过一个小时来完成!
可能你在想:“这是调用者的错,他应该更仔细地读文档。”有道理,但在本例中,
l i s t.s i z e()不是一个固定时间的操作,这一点是出人意料的。所有其他的C + + 容器类的
size()方法都是时间固定的。
假使s i z e()的名字是c o u n t S i z e()或者c o u n t E l e m e n t s(),很可能就会避免相同的错误。
C + + 标准库的作者可能是希望把它命名为s i z e()以和所有其他的容器一致,就像v e c t o r和
m a p 。但是正因为他们的这个选择使得程序员很容易误把它当成一个快速的操作,就像
其他的容器一样。谢天谢地,现在最新的C + + 标准库把size()改成了O(1) 。
向导是谁
一段时间以前,有位作者正在安装O p e n B S D 操作系统。在磁盘格式化这一步
时,出现了一个复杂的菜单,询问磁盘参数。其中的一个选项是进入“向导
模式”(W i z a r d m o d e )。他看到这个友好的选择松了一口气,并选择了它。
让他失望的是,安装程序给出了低层命名行提示符等待手动输入磁盘格式化
命令,而且也没有明显的方法可以退出。很明显,这里的“向导”指的是你
自己。