为什么不写注释?写“为什么不”注释?
原文:Hillel - 2024.09.10
代码是用结构化的机器语言编写的,而注释是用富有表现力的人类语言编写的。人类语言让注释比代码更具表达性和沟通能力。代码中也包含少量类似于人类语言的内容,例如标识符。所谓“注释要写为什么,而不是写做了什么”,意思是尽可能将信息嵌入到标识符中。并非所有“做了什么”都能这样嵌入,但很多都可以。
近年来,我看到越来越多的人主张,连“为什么”也不应该出现在注释中,它们可以通过LongFunctionNames
(长函数名)或测试用例的名称体现出来。几乎所有“自解释”代码库都通过增加标识符来进行文档化。
那么,有哪些人类表达的内容是无法通过更多代码来呈现的呢?
反面信息,也就是引起注意系统中“没有的”东西。“为什么不”的问题。
一个近期的例子
这是一个来自《Logic for Programmers》的例子。由于技术上的复杂原因,epub 电子书构建过程中未能将数学符号(\forall
)正确转换为符号(∀
)。我写了一个脚本,手动遍历并将数学字符串中的标记替换为对应的 Unicode 等价符号。最简单的方法是对每个需要替换的 16 个数学符号依次调用string = string.replace(old, new)
(一些数学字符串包含多个符号)。
这种方法效率非常低,我可以将所有 16 个替换在一次遍历中完成。但那将是一个更复杂的解决方案。因此,我选择了简单的方法,并加了一条注释:
对每个字符串进行了 16 次遍历。
整本书中只有 25 个数学字符串,大多数字符少于 5 个。
因此,速度仍然足够快。
你可以把这看作是“为什么我用了慢的代码”的解释,但也可以理解为“为什么不用快的代码”。它引起了对“没有的东西”的关注。
为什么要有注释
如果慢速代码没有造成任何问题,为什么还要写注释呢?
首先,这段代码可能以后会成为问题。如果将来的《Logic for Programmers》版本中有上百个数学字符串,而不是几十个,这个构建步骤将成为整个构建过程的瓶颈。现在留下标注,方便将来知道该修复什么。
即使这段代码永远不会有问题,注释仍然很重要:它表明我意识到了权衡。假设两年后我回到这个项目,打开epub_math_fixer.py
,看到我这段糟糕的慢代码。我会问自己:“当时为什么写了这么糟糕的代码?” 是因为缺乏经验,时间紧迫,还是纯粹的随机失误?
这条反面注释告诉我,我知道这段代码很慢,考虑过替代方案,并决定不做优化。这样,我不必花大量时间重新调查,却得出同样的结论。
为什么这不能通过代码“自解释”(self-documented)
当我第一次尝试这个想法时,有人告诉我,我的反面注释没有必要,只需将函数命名为RunFewerTimesSlowerAndSimplerAlgorithmAfterConsideringTradeOffs
。除了名字过长、未解释权衡点,并且如果我优化了代码,还得在所有地方修改函数名外……这实际上使代码更不能自解释。因为它没有告诉你函数实际做了什么。
核心问题在于,函数和变量的标识符只能包含一条信息。我无法在一个标识符中同时存储“函数做了什么”和“它作出了什么权衡”。
那么用测试代替注释呢?我猜你可以写一个测试,用grep
查找书中的数学块,并在超过 80 个时失败?但这并没有直接测试EpubMathFixer
。函数本身没有任何内容可以让你直接关联上。
这是“自解释”反面信息的根本问题。“自解释”是伴随代码书写的,它描述了代码在做什么。而反面信息是关于代码没有做什么的。
最后的思考
我在想,是否可以将“为什么不”注释视为反事实的一个例子。如果是这样,那么“人类沟通的抽象”是否一般都无法“自解释”?你能“自解释”一个比喻吗?不确定性呢?伦理主张呢?