原文Hillel - 2024.09.10

代码是用结构化的机器语言编写的,而注释是用富有表现力的人类语言编写的。人类语言让注释比代码更具表达性和沟通能力。代码中也包含少量类似于人类语言的内容,例如标识符。所谓“注释要写为什么,而不是写做了什么”,意思是尽可能将信息嵌入到标识符中。并非所有“做了什么”都能这样嵌入,但很多都可以。

近年来,我看到越来越多的人主张,连“为什么”也不应该出现在注释中,它们可以通过LongFunctionNames(长函数名)或测试用例的名称体现出来。几乎所有“自解释”代码库都通过增加标识符来进行文档化。

那么,有哪些人类表达的内容是无法通过更多代码来呈现的呢?

反面信息,也就是引起注意系统中“没有的”东西。“为什么不”的问题。

一个近期的例子

这是一个来自《Logic for Programmers》的例子。由于技术上的复杂原因,epub 电子书构建过程中未能将数学符号(\forall)正确转换为符号()。我写了一个脚本,手动遍历并将数学字符串中的标记替换为对应的 Unicode 等价符号。最简单的方法是对每个需要替换的 16 个数学符号依次调用string = string.replace(old, new)(一些数学字符串包含多个符号)。

这种方法效率非常低,我可以将所有 16 个替换在一次遍历中完成。但那将是一个更复杂的解决方案。因此,我选择了简单的方法,并加了一条注释:

1
2
3
对每个字符串进行了 16 次遍历。
整本书中只有 25 个数学字符串,大多数字符少于 5 个。
因此,速度仍然足够快。

你可以把这看作是“为什么我用了慢的代码”的解释,但也可以理解为“为什么不用快的代码”。它引起了对“没有的东西”的关注。

为什么要有注释

如果慢速代码没有造成任何问题,为什么还要写注释呢?

首先,这段代码可能以后会成为问题。如果将来的《Logic for Programmers》版本中有上百个数学字符串,而不是几十个,这个构建步骤将成为整个构建过程的瓶颈。现在留下标注,方便将来知道该修复什么。

即使这段代码永远不会有问题,注释仍然很重要:它表明我意识到了权衡。假设两年后我回到这个项目,打开epub_math_fixer.py,看到我这段糟糕的慢代码。我会问自己:“当时为什么写了这么糟糕的代码?” 是因为缺乏经验,时间紧迫,还是纯粹的随机失误?

这条反面注释告诉我,我知道这段代码很慢,考虑过替代方案,并决定不做优化。这样,我不必花大量时间重新调查,却得出同样的结论。

为什么这不能通过代码“自解释”(self-documented)

当我第一次尝试这个想法时,有人告诉我,我的反面注释没有必要,只需将函数命名为RunFewerTimesSlowerAndSimplerAlgorithmAfterConsideringTradeOffs。除了名字过长、未解释权衡点,并且如果我优化了代码,还得在所有地方修改函数名外……这实际上使代码更不能自解释。因为它没有告诉你函数实际做了什么

核心问题在于,函数和变量的标识符只能包含一条信息。我无法在一个标识符中同时存储“函数做了什么”和“它作出了什么权衡”。

那么用测试代替注释呢?我猜你可以写一个测试,用grep查找书中的数学块,并在超过 80 个时失败?但这并没有直接测试EpubMathFixer。函数本身没有任何内容可以让你直接关联上。

这是“自解释”反面信息的根本问题。“自解释”是伴随代码书写的,它描述了代码在做什么。而反面信息是关于代码没有做什么的。

最后的思考

我在想,是否可以将“为什么不”注释视为反事实的一个例子。如果是这样,那么“人类沟通的抽象”是否一般都无法“自解释”?你能“自解释”一个比喻吗?不确定性呢?伦理主张呢?