Skip to content
Published on

代码中的命名:最难的简单事

分享
Authors

引言 — 命名为什么这么难

菲尔·卡尔顿(Phil Karlton)有一句名言:"计算机科学中只有两件难事:缓存失效,以及命名。"第一次听到时,这话听起来像是一句俏皮的文字游戏。命名居然和缓存失效一样难?不就是给一个变量安上一个词吗?

可是写过几年代码之后,这句玩笑就会变得扎心。命名之所以难,不是因为打字难,而是因为命名的那一刻,我们是否真正理解了这个概念就会暴露无遗。如果一个函数怎么也想不出好名字,那多半说明这个函数不止做了一件事,或者我们自己都还没把它的职责想清楚。命名是思考的工具,不是装饰。

本文从实务角度梳理几条命名的原则。目的不是让人死记规则 — 只要理解每条原则为什么存在,遇到陌生场景时也能自己做出判断。

名字应当揭示意图,而不是实现

如果只能留下一条原则,那就是这条:好名字说的是"为什么存在",而不是"做什么"。 读者不需要翻看实现,仅凭名字就该明白这个值或函数的含义。

先看一个反例。

# d 是什么?得看实现才知道
d = 30

# 遍历列表,挑出满足某个条件的元素,装进新列表
list1 = []
for x in list2:
    if x[3] == 1:
        list1.append(x)

dlist1list2x[3] == 1 没有一个能自己说明白。要看懂这段代码,得把周围的上下文全部读一遍。现在把它们换成能揭示意图的名字。

GRACE_PERIOD_DAYS = 30

active_users = []
for user in all_users:
    if user.is_active:
        active_users.append(user)

变的不是逻辑,只是名字。可代码却能自己说明白了。数字 30 意味着什么、我们为什么要挑出这些用户,都已经写进了名字里。不再需要一条"这是30天的宽限期"的注释。好名字能替代注释。

名字的长度与作用域成正比

新手常见两种极端。一种是把所有东西都缩写成 abtmp;另一种正相反,取像 theListOfAllActiveUsersInTheCurrentOrganization 这样一整句话长的名字。两种都不好。

一个可靠的规则是:名字的长度应当与它的作用域(存活范围)成正比。

# 短作用域:只活在单行推导式里的变量,短一点没关系
squares = [n * n for n in numbers]

# 广作用域:整个类都在用的字段、或模块级全局变量,要足够有描述性
class PaymentProcessor:
    def __init__(self):
        self.pending_settlement_count = 0

只存在于一行之内的循环变量 n 短一点无妨,因为眼睛能一眼看完这么小的范围。反过来,一个贯穿整个类的字段如果叫 n,那就是灾难 — 要追踪这个值在哪里、怎么被使用,名字自己就得背负上下文。

归纳一下:循环下标和短 lambda 的参数可以短,函数参数适中,作用域越广、存活越久的名字就应该越有描述性。作用域越广,名字要承担的说明责任就越重。

避免编码信息和匈牙利命名法

曾经流行一时的匈牙利命名法,是在变量名前面加上类型信息作前缀,比如 strName(字符串 name)、iCount(整数 count)、arrUsers(数组 users)。如今大多数人把它视为反模式。

# 匈牙利命名法 - 应当避免
strUserName = "alice"
iRetryCount = 3
arrActiveUsers = [...]

# 类型交给语言和 IDE 去告知,名字专注于含义
user_name = "alice"
retry_count = 3
active_users = [...]

原因有两个。第一,现代语言的类型系统和 IDE 已经会告诉你类型,在名字里重复类型信息是多余的。第二,也更危险的是:类型一旦变化,名字就开始说谎。如果后来把 arrUsers 改成集合(set)或字典,前缀 arr 就成了错误信息。名字应当永远为真,而把实现细节编码进名字,恰恰让这份"真"很容易被打破。

出于同样的理由,给成员变量加 m_ 前缀、给全局变量加 g_ 前缀的做法,现在也不常见了。真要区分的话,self.count 这类语言自带的语法已经足够。

使用可搜索的名字

代码被阅读和查找的时间,远比被写下的时间长得多。所以名字是否可搜索(是否好 grep),是一条比看上去更重要的实务标准。

假设你把数字字面量 7 散落地写在代码各处。后来发现这个 7 表示"一周",想要修改它的值,在整个项目里搜索 7,只会翻出一堆毫不相关的 7。但如果提前给它取了名字,就能精确定位到这个概念本身。

# 差:很难看出这个7是什么意思,也不知道还出现在哪里
if days_since_login > 7:
    send_reminder()

# 好:按名字搜索,精确定位到这个概念被使用的地方
INACTIVE_THRESHOLD_DAYS = 7
if days_since_login > INACTIVE_THRESHOLD_DAYS:
    send_reminder()

同理,单字母变量和过于常见的词也会妨碍搜索。如果 datavalueitem 这类名字在代码库里出现几百次,要找到那个特定的值就很困难。名字越独特、越具体,越能帮到未来的自己。

布尔值、函数、集合的命名惯例

有几条已经沉淀下来的惯例值得掌握,能让整个团队的代码更容易读。

布尔值要用能以"是/否"回答的名字。 加上 ishascanshould 这类前缀,在条件语句里读起来会很自然。

# 光看名字就是一个真/假的问题
is_active = True
has_permission = user.role == "admin"
can_retry = attempt < max_attempts

if is_active and has_permission:
    ...

如果用 activepermission 这样的名词形式,if active: 就分不清是在问"是否处于活跃状态?"还是在命令"把它设为活跃"。一个前缀就能消除这种歧义。

函数名以动词开头。 因为函数代表的是动作,比如 getUsercalculateTotalsendEmail。相对地,承载值的变量是名词。而且,名字最好也能透露出返回值是什么 — 读者会自然而然地期待 getUser() 返回一个用户、isValid() 返回一个布尔值。

集合用复数形式。 承载多个元素的列表或集合,用复数形式的名字。

# 单数是一个,复数是多个 - 遍历时读起来很自然
users = fetch_all_users()
for user in users:
    notify(user)

user 是一个,users 是多个。得益于这条简单的规则,for user in users: 读起来就像一句话。相反,像 for u in userList: 这样的代码,每次都得停下来解读一下。

重命名是一次独立的重构

很多人卡在这一步:"我知道现在的名字不太好,但一改就得动一大片……"于是烂名字就像化石一样留了下来。

但重命名恰恰是现代工具里最安全的重构之一。IDE 的"重命名符号"功能不是简单的字符串替换,它理解语言语义,只精确改动那个符号被引用的地方,不会动到同名却无关的其他变量。

原则很简单:一旦想到更好的名字,那一刻就是该改的时候。 最好把改名字的提交和改逻辑的提交分开,不要混在一起,这样审阅者一眼就能确认"这只是一次纯粹的重命名,行为没有变化"。

  好习惯:
  提交1: rename calc() -> calculate_monthly_total()   (行为无变化)
  提交2: 实际的逻辑修改                                  (行为有变化)

  坏习惯:
  提交1: 改名字又改逻辑还挪了字段……                    (无法审阅)

不要害怕修改名字。代码是一份活文档,理解越深入,就越能配上更好的名字。不必一开始就追求完美的名字 — 让名字随着理解一起成长就好。

常见的坑

最后再列举几个实务中常遇到的命名陷阱。

  • 撒谎的名字:函数名叫 getUser,内部却在创建或删除用户,这个名字就是在撒谎。查询类函数被默认期待没有副作用,所以这种背叛尤其危险。
  • 不一致的词汇:同一个概念,有的地方叫 fetch,有的地方叫 get,有的地方叫 retrieve,读者就会疑惑"这三个是不是不一样?"在同一个代码库里,一个概念定一个词,然后坚持用下去。
  • 无意义的赘词UserDataUserInfoUserObject 里的 DataInfoObject 什么也没加。很多时候,光是 User 就够了。
  • 念不出来的名字:像 genymdhms(generation year month day hour minute second)这样的名字,和同事对话时根本没法念出声。代码是团队要一起讨论的对象,能不能读出来也很重要。

结语

命名之所以是"最难的简单事",是因为它在语法上没有任何限制,却把我们的理解水平原原本本地暴露出来。好名字说的是意图而不是实现,长度与作用域成正比,不编码类型信息,可搜索,并且遵循团队的惯例。

最重要的是,名字从来不是固定不变的。一旦想到更好的名字,重命名随时都是一次安全的重构。与其从一开始就苦苦追求完美的名字,不如先取一个"以目前的理解来说最合适"的名字,再随着理解加深不断打磨 — 这样健康得多。归根结底,好名字积累起来,代码就会变成一篇不需要额外说明书就能读懂的文章。

参考资料

  • Robert C. Martin, "Clean Code"(第2章:Meaningful Names)
  • Phil Karlton 引言的背景: https://www.karlton.org/2017/12/naming-things-hard/
  • Tim Ottinger, "Ottinger's Rules for Variable and Class Naming"
  • Kernighan & Pike, "The Practice of Programming"(关于清晰与命名)