变量命名指南

变量命名指南

题图:画一只漂亮的蝴蝶是很困难的事。)

There are only two hard things in Computer Science: cache invalidation and naming things.

-- Phil Karlton

就像上一篇文章提到过的,计算机科学中最难应对的两件事之一,就是命名。今天正好看到一篇博文,总结的不错,虽然《代码大全》中也有讲过变量如何命名,但是感觉有点死板,不如该文的思路灵活,在自己学习的过程中顺便翻译出来分享给大家。

本文为意译,感觉原文罗嗦的地方就省略了。

选择良好的变量名,方便人们梳理代码,帮助理解代码意图,专注于CodeReview。

为什么命名变量

让变量的命名更有意义是为了让人们理解。完全无意义自动生成命名的代码对计算机来说是无妨的,比如:

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

代码本来就是在‘process’事情(毕竟,代码就是运行在进程-processor-里),所以process这七个字符就是浪费,跟“计算”重复表意了。“element”也不是很好,虽然暗示是对集合进行操作,但是这已经很明显了。甚至代码里有bug,这样的命名也不能帮助读者准确定位。

  • numResults

大多数的代码都会生成“结果(result)”,所以,和process一样,也是浪费了7个字符。整个变量名numResults表明它旨在限制输出的数量,但它含糊的表意又增加了读者的心智负担。

  • collection

浪费空间,前面的标识很明显是Collection<Integer>。

  • num

仅仅是对象类型(int)的重复。

  • result, count

跟numResults一样是浪费空间,它们并不能帮助读者理解代码。


无论如何,请记住变量名的真正目的:帮助读者理解代码。要做到这点,必须遵循以下两点:

  1. 编码者的意图是什么?
  2. 代码实际上做了什么?


示例中使用的变量名有多长要看读者的心理预期了,下面是重写的该函数示例,展示了读者实际可以从命名中获得什么:
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;
}

这次命名修改似乎比自动生成的还糟,至少那个更短。这次重构想展现的编码者的意图依旧模糊,而且代码审查者需要看更多的字符了,代码审查者需要审查很多代码,不好的名字使得这份工作难上加难。我们如何为代码评审减负呢?

关于代码评审

代码评审者承受的心智负担来自于两个方面:距离(distance) 和 废话( boilerplate,那些已经形成公式化的经常被使用的无用无意义的命名,比如num,string之类)。距离,是指评审者的视觉范围,用于识别声明的变量到底在代码中做了些什么。评审者缺乏编码者在写代码时候的上下文信息,评审者必须飞快重新构建上下文,因为没有必要在审查上花和写代码一样长的时间(当然也有例外,比如一些复杂的算法协议等)。良好的命名能消除距离的问题,因为可以提示评审者它们的目的,这样就不必再返回查看代码前面的部分了。

另一个负担是 废话。代码一般做的事情比较复杂,它是由别人编写的。评审者通常从自己的代码中切换到别人的代码中,他们每天要审查很多代码,并且可能已经审查很多年了。所以,审查者必须在代码审查期间保持专注。因此,每个无用的字符都在消耗审查者的效率。对于个别小的例子,代码不清无所谓,花一定的精力和时间总能搞清楚。但是总不能一遍又一遍,一年又一年这样做。这样是很折磨人的( It's death by 1,000 cuts.)。

一个好的例子

所以,为了用最少的字符向代码评审者传达意图,编码者可以向下面这样重写代码:

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

函数的名称已经很明显了,这里就不需要复杂的名字了

c

collection减少9个字符来节省审查者查看样板字符的心智负担 。因为函数很短,并且只涉及一个collection,所以很容易记住c是整数的集合。

skipped

不像results,现在是自带文档,它表示的就是返回值本来的意义。因为该函数比较短,并且skipped声明是一个int,叫它numSkipped则会浪费3个字符。

i

for循环使用i迭代是一个惯用法,人人都能理解。比用count强,起码能节省4个字符。

maybePositive

num跟int做同样的事,而maybePositive是很难被误解的,也可以帮助定位bug。


现在,可以更容易的看出代码中有个bug。原始版本的代码中,不清楚编码者是否只打算打印正整数。现在读者可以发现那有一个bug,因为0不是正数(所以n应该大于0,而不是大于等于)。(原文此处还有两句,似乎有问题,省略那部分内容了)


命名原则(除非你知道更好的)

  • 作为代码编写者,我们的工作是与人类交流,而非计算机
  • 不要让别人猜。命名应该能传达编码者的意图,不要让读者不得不试图花精力搞清楚。
  • 代码审查是必要的,但是需要耗费精力。‘废话’的使用必须最小化,因为它会消减审查者专注于代码的能力。
  • 好的命名胜过注释,但不能取代所有注释。

Cookbook

为了实现这些原则,下面是一些编写代码时的实用指南。

不要把类型放到名称中

把变量的类型放到变量的名称中会增加读者的心智负担(需要查看更多废话),并且经常会妨碍你思考一个更好的名字。像Eclipse这种现代编辑器更方便的展示变量的类型,但会导致把类型添加到命名名称中去。这种做法会导致错误,我曾经见过这样的代码:

Set<Host> hostList = hostSvc.getHosts(zone);

最常见的错误是将Str或String附加到名称后,或者在名称中包含集合类型。这里有一些建议:

更一般的情况:

  • 复数化变量名称,而不是包含集合类型的名称
  • 如果总想在变量名中添加标量类型(int、String、Char),你应该:

* 更好的解释变量是什么

* 解释你导出新变量所做的转换工作

常用Teutonic(日耳曼)命名

大多数的命名应该Teutonic,像挪威语那样遵循语言的精神,而不是像英语那样含糊不清。挪威语有更多像tannlege(字面意思是‘牙医’)和sykehus(字面意思是‘病房’)这样的词,而像dentist 和 hospital(容易混淆,除非你知道他们的意思)这样的词则很少。应该努力使用Teutonic精神来命名变量,使得理解起来更加直接。(译注:就是尽量少用容易产生歧义的词)

另一个启发是,命名方式要尽可能的具体。例如,一个函数被硬编码为只检查CPU是否过载,则将其命名为overloadedCPUFinder,而不是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) {  }

Teutonic命名约定的例外在本节稍后部分再介绍:惯用语和短变量名

还值得注意的是,本节并不建议你不要使用通用名称。对于某种通用功能的代码应该使用通用的名称。比如下面示例中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;

避免废话

下面这些变量永远都不应该去使用,他们已经被滥用了很多年了:

  • val, value
  • result, res, retval
  • tmp, temp
  • count
  • str

这些变量名加类型的变体也应该中止使用,比如tempString或intStr等。

在显而易见的地方使用惯用语

  • 在循环中直接使用i, j 和k
  • 使用n作为限制/数量
  • 异常处理中使用e

// OK
for (int i = 0; i < hosts.size(); i++) { }

// OK
String repeat(String s, int n);

警告: 惯用语只能在明显适合它本意的情况下使用

距离比较短的时候使用短命名

在某些情况下,优先选用短命名,甚至单字母。当评审者看到一个很长的名字时,往往会认为它们值得被注意,如果该名字完全无用的时候,则会浪费时间。一个简短的名字唯一能传达的意义是变量的类型。因此,当满足下面三个条件的时候,可以用短命名(一个或两个字母):

1. 声明和使用的距离不算太远(5行以内,声明应该在读者的视野中)

2. 没有比变量的类型更适合的名称了

3. 读者并不需要记住那个位置的代码(研究表明,人类可以记住约7件事)

这有个例子:

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这个长命名向评审者传达出引其重视的信号,然而评审者浪费他的精力确定该名字只是个废话。这样一个例子不是什么大事,但请记住,代码评审是一种折磨(Death of a thousand cuts)。

删除无关的一次性变量

一次性变量(OTVs, One-time-Variables,也称为垃圾变量)是被用于在函数间传递中间结果的那些变量。有时候是有目的地去使用它们,有的时候却没有一点用处,只会让代码更乱。在下面的代码片段中,编码者制造了阅读障碍:

List<Host> results = hostService.fetchMatchingHosts(hostFilter);
return dropletFinder(results);

可以简化为:

return dropletFinder(hostService.fetchMatchingHosts(hostFilter));

用短的OTVs拆分长代码

有时候你需要用一次性变量来帮助拆分一行较长的代码。

List<Host> hs = hostService.fetchMatchingHosts(hostFilter);
return DropletFinderFactoryFactory().getFactory().getFinder(region).dropletFinder(hs);

这样就很好,因为距离很短,你可以使用OTVs,用两个字母hs来帮助减少视觉混乱。

使用短的OTVs来拆分复杂表达式

下面这代码很难读:

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);

因为距离很短,含义很明显,所以可以使用短命名。

用较长的OTVs解释混乱的代码

有时候你需要OTVs来简单的解释代码是做什么的。比如,有时候你不得不使用别人写的垃圾命名代码(哼!),所以你不能修改这个名称。但是你能使用一个有意义的OTVs来解释这一切。比如:

List<Column> pivotedColumns = olapCube.transformAlpha();
updateDisplay(pivotedColumns);

注意这种情况你不应该使用短的OTV:

List<Column> ps = olapCube.transformAlpha();
updateDisplay(ps);

这只是增加了视觉混乱,并没有对代码的解释提供多少帮助。

编辑于 2016-12-09

文章被以下专栏收录