原文
命名是如此重要。 如果你的代码至少要被阅读一次(如果只有你自己阅读的话),那么名称将在你使用它时扮演重要角色。 变量名称,函数名称,类名称,接口名称,都是无价的方法来让你的代码更多地描述它们在做什么。 在工作中进行代码审查时,我和团队成员对好命名非常挑剔(伙计们,对此表示抱歉!),但我相信命名会提高或损害我们代码的质量。
尽管还有其他方法可以知道一段代码在做什么,例如文档;但出于以下两个原因,好的命名是传递有关代码信息的极其有效的渠道:
- 非常好的名字会立即告诉你周边代码在做什么,而不是查找文档并通过它来查看代码
- 命名可以快速得到改善。 你可以通过手动或使用工具(例如流行的clang-tidy)进行快速修复来更新代码中的某些名称,并且如果你的代码是可构建的,则几乎可以肯定它会通过测试。
这篇文章旨在提供有关如何选择好名字的指南。 我已经从Steve McConnell Code Complete的参考书中删除了其中一些指南(如果你尚未阅读它,我建议停止阅读本文或其他正在做的事情,然后开始阅读本书)。 我从与同事一起工作的讨论,建议和代码审查中学到了其他一些知识。 多年来,我通过自己的努力,通过阅读和编写代码尝试各种不同的事情。
我们将从告诉您如何避免使用不良名字开始,然后着重于如何挑选好名字。
不要做任何非法的事情
有些命名在C++中是不允许使用的
除了使用标准保留的名称(如“ int”)将停止编译外,名称中的下划线(_)的某些组合将在不合法的情况下进行编译,因为它们是为编译器或标准库实现者保留的。 使用它们可能会与它们声明的对象或例程冲突,从而导致细微的错误和意外的行为。
这是为编译器和标准库实现者保留的名称:
- 带有两个下划线(__)的任何名称,
- 任何以一个下划线开头的名称,后跟一个大写字母(_isOk,isOk_too,_IsNotOk),
- 以一个下划线开头并在全局命名空间中的名称。
因此,不要考虑使用此类名称,因为它们可能会给你带来麻烦。
不要浪费信息
想想看,你的代码完全知道它在做什么。实际上,它是最了解的:它尽可能忠实地执行其中的内容!
给出好的命名实际上是在尽可能多地保留这些信息。换句话说,这不是通过混淆代码来浪费信息。有趣的是,通常鼓励通过封装来隐藏信息。但是在这种情况下,你想要针对的只是信息披露。
因此,请限制使用缩写词。缩写词和首字母缩写词写起来很方便,但很难阅读。俗话说,代码只写一次,却要读很多遍。现在,你不必系统地拼写所有首字母缩写词以使代码更清晰,而某些重复的未缩写代码甚至可能会损害可读性。例如,在你的代码中似乎合理地使用了“ VAT”,而不是每次使用它时都写valueAddedTax,因为每个人都知道VAT是什么。(嗯,其实我们真不知道啥是含税价格。。。)
如何选择在代码中是否使用首字母缩写词?一个好的经验法则是,如果你的应用程序的最终用户可以理解特定的缩写或首字母缩写,那么可以在代码中使用它,因为这表明你所在领域的每个人都知道这意味着什么。
不要尝试针对最小字符数进行优化。在论坛上,你会看到有人认为他们的方法更好,因为它涉及的打字更少。但是,哪个更麻烦,几次击键还是盯着代码看几分钟来试图理解它的意思呢?
对于函数和方法名称,尤其如此,你可以根据需要进行设置。研究表明(Rees 1982),函数和方法的名称最多可以包含35个字符,这听起来确实很多。
但是,函数名称的长度也会由于不好的原因变得过大:
如果某个函数的名称因该函数执行过多操作而过长,则解决方法不是在命名上,而是通过将其分解为几个逻辑部分。
-
当函数名称包含的多余信息已经由其参数类型表示时,它们就会人为膨胀。 例如:
void saveEmployee(Employee const& employee);
可以重命名为:
void save(Employee const& employee);
这会引导调用者写出更自然的代码:
save(manager);
而不是:
saveEmployee(manager);
这与接口原则和ADL(关于调用者删除多余的名称空间)一致,后者将成为专门帖子的主题。
-
命名包含不必要的信息的另一个原因是它包含否定词。 如下代码:
if (isNotValid(id)) {
可以通过使用肯定命名来改进:
if (!isValid(id)) {
现在,我们已经排除了一些不良的命名习惯,让我们集中讨论如何挑选好名字。
选择与抽象级别一致的名称
如之前的文章所述(一切都归结为尊重抽象级别),尊重抽象级别是许多良好实践的根本。 这些做法之一就是好的命名。
好的命名是与周围代码的抽象级别一致的名称。 就像在抽象级别的帖子中解释的那样,可以用不同的方式来表达:好的命名表示代码在做什么,而不是怎么做。
为了说明这一点,让我们以一个计算公司中所有员工薪水的函数为例。 该函数返回key(员工)与value(工资)相关联的结果的集合。 这个代码的虚构实现者观看了钱德勒·卡鲁斯(Chandler Carruth)关于数据结构性能的演讲,并决定放弃map,改为使用包含pair的vector。
一个错误的,让人关注函数实现方式的函数名称是:
std::vector< pair > computeSalariesPairVector();
这种函数名称的问题在于它表示该函数以PairVector的形式来计算其结果,而不是专注于其工作,即计算雇员的薪水。 快速解决方案是将名称替换为以下内容:
std::vector< pair > computeEmployeeSalaries();
这样可以使调用者摆脱一些实现细节,让你作为代码阅读者专注于代码打算做什么。
尊重抽象级别会对变量和对象名称产生有趣的影响。在代码的许多情况下,变量和对象表示的内容比其类型所暗示的内容更抽象。
例如,一个int通常代表的不仅仅是一个int:它可以代表一个人的年龄或集合中元素的数量。或者Employee类型的特定对象可以代表团队的经理。或者 std::vector
可以表示过去一个月在纽约观察到的每日平均温度。 (当然,这在非常低级的代码(例如添加两个int或使用强类型的地方)中不适用)。
在这种情况下,你想要基于变量表示的含义命名而不是其类型。你可以将int变量命名为“ age”,而不是“ i”。你将上述员工称为“manager”,而不仅仅是“employee”。你可以将向量命名为“temperatures”,而不是“doubles”。
很明显,至少在两种情况下,我们通常忽略应用此准则:迭代器和模板类型。
尽管随着算法和范围库的发展,迭代器将趋于消失,但是仍然需要一些迭代器,并且无论如何,今天仍有许多迭代器。例如,让我们收集从金融产品支付或收取的现金流量的集合。这些现金流量有些是正数,有些是负数。我们想获取流向我们的第一个现金流量,因此是第一个正数。这是编写此代码的第一次尝试:
std::vector flows = ...
auto it = std::find_if(flows.begin(), flows.end(), isPositive);
std::cout << "Made " it->getValue() << "$, at last!" << std::endl;
该代码使用名称“ it”,以反映其实现方式(使用迭代器),而不是变量的含义。 与以下代码进行比较:
std::vector flows = ...
auto firstPositiveFlow = std::find_if(flows.begin(), flows.end(), isPositive);
std::cout << "Made " << firstPositiveFlow->getValue() << "$, at last!" << std::endl;
哪种代码能让你最省力地理解它? 你能想象你要读的不是两行代码而是10或50行时候的区别么? 请注意,这与我们在上一节中描述的不浪费代码自解释的宝贵信息有关。
相同的逻辑适用于模板参数。 尤其是在开始使用模板时(我们看到的大多数示例都是来自学术界),我们倾向于为所有模板类和函数编写以下代码行:
template
尽管你可能对T的了解不仅限于它是一种类型
在非常通用的代码中,如果你不清楚关于类型的任何信息,使用T作为类型名是很好的,比如在std::is_const中:
template
struct is_const;
但是如果你知道任何关于T表示的含义,你可以将尽可能多的信息写入代码中。我们之前在Fluent C++上一篇专门文章(了解STL
template
T parse(SerializedInput& input)
{
T result;
// ... perform the parsing ...
return result;
}
另外展示一个更准确表示T的含义的例子:
template
ParsedType parse(SerializedInput& input)
{
ParsedType result;
// ... perform the parsing ...
return result;
}
比较这两段代码。 你认为哪一种更容易使用?
你可能会认为这有很大的不同,也可能没有。 但是可以确定的是,第二段代码包含了更多的信息,而且无需付出其他代价。
总的来说,这对于良好的命名是正确的:一旦那里有免费的午餐,就来抢一顿。