软件是写给人来理解的;因此要合理地选择变量名。别人需要梳理你的代码,并且去理解代码的意图,才能够扩展或者修复。既浪费了空间又不直接明了的变量名很多。即使用心良苦,很多工程师最后选的变量名最多也只是徒有其表。这篇文章目的就是帮助工程师如何选取好的变量名。我们侧重于代码评审,因为在代码评审中最容易暴露出糟糕变量名的问题。当然,使用好的变量名还有其他很多原因(比如提高代码可维护性)。
给变量一个有意义的命名主要原因就是能够让人理解这些变量。如果仅仅只是写给机器的话,那么那些自动生成并且没有任何意义的变量也无妨,比如:
int f1(int a1, Collection<Integer> a2) { int a5 = 0; for (int a3 = 0; a3 < a2.size() && a3 < a1; a3++) { int a6 = a2.get(a3); if (a6 >= 0) { System.out.println(a6 + " "); } else { a5++; } } System.out.println("n"); return a5; }
任何一位工程师都看得出上面的代码很难理解,因为它违背了两条准则:1)禁止简写;2)赋予有意义的变量名。不过话说回来,这些准则也不一定总是有用的。缩写也不一定就是不好,之后我们会讨论到。“有意义”这个概念有些模糊,而且取决于不同人的解释。有些工程师认为冗长的就是有意义的(例如MultiDictionaryLanguageProcessorOutput)。有些人则发现理解所谓”真正的“有意义是很困难的,所以在做努力之前就已经放弃了。因此,在尽量满足上述两条准则下,写出的代码可能如下:
int processElements(int numResults, Collection<Integer> collection) { int result = 0; for (int count = 0; count < collection.size() && count < numResults; count++) { int num = collection.get(count); if (num >= 0) { System.out.println(num + " "); } else { result++; } } System.out.println("n"); return result; }
比起第一个例子来,评审人员更容易理解上述代码。变量名很准确而且可读性强。但实际上却没有任何的帮助并且浪费空间,因为:
processElements
几乎所有的代码都是在“处理”事物(毕竟,代码的作用都是“processor”),所以process这个单词其实就是七个没意义的字母,仅仅只是表示“计算”而已。Elements这个词也没有好到哪里去。很显然这个函数是要在集合上进行操作。而且使用这个函数名也不能帮助读者找出bug。
numResults
大多数代码都会产生“结果”(最终都会);所以就像process一样,Results也是七个没意义的字母。完整的变量名,numResults给人感觉像是要限制输出的数量,但是又太含糊让读者很伤脑筋。
collection
浪费空间;很显然这是个集合,因为之前的类型申明就是Collection<Integer>.
num
仅仅就表达这是int类型
result, count
这两个就是编码时的陈腔滥调了;就如numResults一样,它们既浪费了空间也过于空泛,并没有提供帮助来让读者理解这段代码。
然而,我们需要牢记变量名的真正用意:读者需要理解代码,这就需要达到以下两点:
来看一个长变量名的例子是怎么给读者增加精神负担的,下面是重新写好的代码,这段代码很好地展示了什么是让读者去推测变量名:
int doSomethingWithCollectionElements(int numberOfResults, Collection<Integer> integerCollection) { int resultToReturn = 0; for (int variableThatCountsUp = 0; variableThatCountsUp < integerCollection.size() && variableThatCountsUp < numberOfResults; variableThatCountsUp++) { int integerFromCollection = integerCollection.get(count); if (integerFromCollection >= 0) { System.out.println(integerFromCollection + " "); } else { resultToReturn++; } } System.out.println("n"); return resultToReturn; }
这种改变让代码看起来比自动生成的代码都糟糕,至少自动生成的代码更短。这段代码并没有让程序员的意图更明显,甚至需要读者看更多的字符。要知道代码评审需要看很多代码,糟糕的命名使得一项艰巨的任务更加艰巨了。那如何才能减少代码评审负担呢?
代码评审的时候主要有两种精神负担:距离和样板代码。从变量名角度来说,距离的意思是指评审人员需要额外看多少代码才能够理解这个变量的作用。评审人员不会像编码人员写代码的时候那样脑海里有大概轮廓,他们只能快速地自己重建这个轮廓。而且评审人员需要很快完成;因为不值得在评审上花费和编码同样的时间。好的变量名能够很好地解决距离这个问题,因为它们能够提醒评审人员这些变量的目的是什么。那样的话评审人员也不需要花时间去回看之前的代码。
另一个负担就是样板代码。代码经常在做一些复杂的事情;它是其他人写的;评审人员经常会根据自己的代码进行上下文切换;他们每天都要看大量代码并且很有可能评审了多年。介于这些,评审人员很难一直保持精神集中。因此,每一个没用的字符都会消耗评审的效率。对于单独一个小的案例,其实不清楚也不是什么大问题。在有足够的时间和精力(可能需要和编码人员有后续交流)的情况下,评审人员完全可以搞清楚所有代码的作用。但是他们不能年复一年地重复这么做。这相当于将评审人员千刀万剐。
所以,为了能够让代码评审人员理解意图,编码人员在尽可能少用字符情况下可以重写成以下代码:
int printFirstNPositive(int n, Collection<Integer> c) { int skipped = 0; for (int i = 0; i < c.size() && i < n; i++) { int maybePositive = c.get(i); if (maybePositive >= 0) { System.out.println(maybePositive + " "); } else { skipped++; } } System.out.println("n"); return skipped; }
我们一起来分析一下每一个变量看看为什么能够让代码更容易理解:
printFirstNPositive
不像processElements,现在很清楚编码人员写这个函数的目的(并且提供了难得的机会发现bug)
n
有了清晰的函数名,对于n就没必要用个复杂的名字了
c
集合并不值得花费太多精力,所以我们削减了9个字符来减少读者浏览样板字符时的疲劳;介于这个函数很短,而且也只有一个集合变量,所以很容易就记住了c是一个整型的集合
skipped
不像results,现在自己就说明了(不需要注释)返回值是什么。介于这是个很短的函数,并且对skipped声明为一个int类型也很容易看到,如果用numSkipped就会浪费了3个字符。
i
在遍历一个循环的时候,使用i变量是个约定俗成的习惯,每个人都能够理解。姑且不说count这个变量名没一点用,i变量还节省了4个字符。
maybePositive
num仅仅只说明int做的事,然而maybePositive就很难被误解并且可以帮助定位出bug。
现在也更容易发现这段代码里面其实有两个bugs。在最初的版本中,如果编码人员只是想打印出正整数的话是很难发现。现在读者们可以注意到一个bug就是0并不是正数(所以n应该大于0,而不是大于等于)。(这里应该也需要单元测试)。此外,因为第一个参数现在是maxToPrint(相反的,maxToConsider),很显然如果集合里面有非正整数的话,函数不会打印出足够的元素。如何正确重写这个函数将留个读者作为练习。
命名的原则
为了满足这些原则,写代码的时候可以使用下面一些实用指南:
把变量的类型放到变量名中会加重读者的精神疲劳(需要扫描更多的样板代码)而且也不是一个好的替换。现在的编辑器比如Eclipse能够很好地展示变量的类型,使得添加类型到命名中很累赘、这种做法也会招致一些错误,我就看过下面这种代码:
Set<Host> hostList = hostSvc.getHost
最容易犯的错误就是在名字后面添加Str或者String,或者加入集合的类型。这里有一些建议:
Bad Name(s) | Good Name(s) |
hostList, hostSet | hosts, validHosts |
hostInputStream | rawHostData |
hostStr, hostString | hostText, hostJson, hostKey |
valueString | firstName, lowercasedSKU |
intPort | portNumber |
一般来说:
大多数命名都应该用日耳曼语系,它遵循了像挪威语那样的优点,而不是晦涩含糊如英语一样的罗曼语系。挪威语里面有更多像tannlege(“牙医”)和sykehus(“病房”)的单词,很少如dentist和hospital这类单词(这类单词拆分之后就不是英语单词了,除非你知道它们的意思,不然这就很难理解)。你应该尽可能使用日耳曼语系的优点来给你的变量命名:即使不认识的情况下也容易理解。
另一种方式使用日耳曼语系名字是在没有错误情况下尽可能的具体。比如,如果一个函数仅仅用于检测CPU的负荷,那就把这个函数命名为overloadedCPUFinder,而不是unhealthyHostFinder。虽然这个函数可能是被用于查找不正常的主机,但是unhealthyHostFinder会使得听起来比其本身更笼统。
// GOOD Set<Host> overloadedCPUs = hostStatsTable.search(overCPUUtilizationThreshold); // BAD Set<Host> matches = hostStatsTable.search(criteria); // GOOD List<String> lowercasedNames = people.apply(Transformers.toLowerCase()); // BAD List<String> newstrs = people.apply(Transformers.toLowerCase()); // GOOD Set<Host> findUnhealthyHosts(Set<Host> droplets) { } // BAD Set<Host> processHosts(Set<Host> hosts) { }
日耳曼语系命名也有例外,在这部分的后面也会提到:习语和短变量名
值得一提的是这里也不是说禁止使用笼统的命名。那些确实是在做一些一般性工作的代码就可以用个笼统的命名。例如,在下面这个例子中的transform是可以的,因为这是一个一般性字符串操作库里面的一部分。
class StringTransformer { String transform(String input, TransformerChain additionalTransformers); }
像之前所说的,变量名是无法(也是不应该)代替注释的。如果用变量名代替一条注释,那这也是很可以的,因为:
例如,
// BAD String name; // First and last name // GOOD String fullName; // BAD int port; // TCP port number // GOOD int tcpPort; // BAD // This is derived from the JSON header String authCode; // GOOD String jsonHeaderAuthCode;
除了不使用日耳曼语系命名外,以下的这些变量名被广泛滥用了很多年,而实际上这些变量名从来不应该被使用:
还有仅仅只是在变量名加上其类型名称也是要被禁止的,比如像tempString或者intStr这类等等
不像之前所说的陈词滥调,有一些习惯的用法是被广泛理解而且能够被安全使用即使是字面上看含义有些模糊。这里有一些事例(都是些Jave/C的例子,但是也适用于其他所有语言):
// OK for (int i = 0; i < hosts.size(); i++) { } // OK String repeat(String s, int n);
警告:习语应该只有在用意明显的时候被使用到
短的命名甚至是一个字母的变量名在某些场合中是更好的。当评审人员看到一个很长的名字,他们就会觉得需要去注意这些长的命名,如果最后发现这个命名完全没用,那纯属于浪费时间。一个短的命名表达了唯一需要了解这个变量的就是它的类型。所以在一下两个成立的情况下,使用短名字(一个或者两个字母)的完全合理的:
这里有个例子:
void updateJVMs(HostService s, Filter obsoleteVersions) { // 'hs' is only used once, and is "obviously" a HostService List<Host> hs = s.fetchHostsWithPackage(obsoleteVersions); // 'hs' is used only within field of vision of reader Workflow w = startUpdateWorkflow(hs); try { w.wait(); } finally { w.close(); } }
也可以写成:
void updateJVMs(HostSevice hostService, Filter obsoleteVersions) { List<Host> hosts = hostService.fetchHostsWithPackage(obsoleteVersions); Workflow updateWorkflow = startUpdateWorkflow(hosts); try { updateWorkflow.wait(); } finally { updateWorkflow.close(); } }
但是这种写法需要占据更多的空间却没有其他的收益;变量使用都很紧凑,读者们也没有问题去理解其用意。还有就是,长命名updateWorkflow表示着这个变量名有特别之处。评审人员要花费精力去看这个命名是不是样板。在这里可能不是个大问题,但是记住,代码评审会“死于”这种“千刀万剐”。
一次性变量,也被成为垃圾变量,是指那些被迫用于函数传递间的中间结果。他们有时也是有用的(详见下一准则),但是大多数时候都是无意义的,并且也会使得代码库混乱。下面的代码段中,编码人员就让读者读起来更艰难:
List<Host> results = hostService.fetchMatchingHosts(hostFilter); return dropletFinder(results);
相对于上面的代码,编码人员应该将代码简化成:
return dropletFinder(hostService.fetchMatchingHosts(hostFilter));
有时需要一个一次性变量来拆分长的代码行
List<Host> hs = hostService.fetchMatchingHosts(hostFilter); return DropletFinderFactoryFactory().getFactory().getFinder(region).dropletFinder(hs);
这是可以的,由于两行代码距离不远,可以使用一次性变量和一个或者两个字母的变量名(hs)来减少视觉混乱。
读这段代码会很困难:
return hostUpdater.updateJVM(hostService.fetchMatchingHosts(hostFilter), JVMServiceFactory.factory(region).getApprovedVersions(), RegionFactory.factory(region).getZones());
写成这样就会更好:
List<Host> hs = hostService.fetchMatchingHosts(hostFilter); Set<JVMVersions> js = JVMServiceFactory.factory(region).getApprovedVersions(), List<AvailabilityZone> zs = RegionFactory(region).getZones(); return hostUpdater.updateJVM(hs, js, zs);
重申一下,因为代码间距离很短而且意图明显,短的变量名完全可以使用。
有时你需要一次性代码简单地解释这段代码在做什么。例如,有时候不得不使用别人写的一些糟糕命名的代码,所以你不能修复这些命名。但是你能做的就是使用有意义的一次性变量来解释在做些什么,比如,
List<Column> pivotedColumns = olapCube.transformAlpha(); updateDisplay(pivotedColumns);
记住,这种情况下你不能使用短的一次性变量:
List<Column> ps = olapCube.transformAlpha(); updateDisplay(ps);
这仅仅只是添加了视觉混乱而并没有帮助有效地解释代码。