跳到主要内容

注释不是代码的欠账,是一笔被转嫁的知识账

深潜
深潜

· 阅读约 8 分钟

上个月底 dev.to 上那篇 Clean Code 和 Clear Code 的帖子,主帖下 107 条评论,吵到最后还是正则怎么逐段拆、BA123 和 99123 谁算有效。这些我不关心。帖子里真正值钱的是一个更小的细节:一个常量 MaxConcurrentRequests 写成了 47,服务商文档白纸黑字写着 50。作者没跟着文档写 50,写了 47,还留了一段注释解释为什么是 47。

这段注释才是那个帖子最值钱的部分。它把“注释有没有用”这个老问题打到最痛的地方:有些知识,代码本身永远装不下。这不是写得清不清楚的问题,是代码这门语言没有给这类知识留位置。

帖子的作者是 Flutter CEE 的一个工程经理,他说了句被转得很多的话:clean code 告诉读者代码做了什么,好注释告诉读者作者知道什么。这句不完整。它把注释和代码放在同一层比较,好像注释只是代码的补充说明。它们根本不是一层。代码描述的是当前状态,是这段逻辑在此时此地的行为;注释要固化的,是这段行为的推理链。缺了后者,后来的人读一段看起来完全正常的代码,会以为它就该这么写,直到改崩了才知道当初有别的约束。

把“代码自解释”当绝对原则,是在同时假设两件事。一个是问题本身结构足够简单,能靠命名和拆分原样映射进代码;另一个是写代码的人做过的决策都能在代码里找到表达形式。真实系统里这两件事经常同时不成立。

帖子里那个航班号正则就是第一件事的反面。用 C# 的 GeneratedRegex 加 partial 方法,输入先 Trim 再转大写,命名也尽量按意思来。但读者还是没法只靠代码看出哪些输入有效。BA123 和 U21234 过,9W5A 过,99123 不过。这些规则不在代码结构里,在 IATA 航班号格式的外部约束里。函数拆得再碎、名字起得再长,也表达不出“前两个字符必须包含至少一个字母”这件事。代码能执行规则,不能解释规则的来源。

第二件事更麻烦。那种被精心重构成“自解释”的代码,每个函数短到三四行,名字读起来像英语句子,仓库干净得像刚从 Clean Code 教材里走出来。但读的人还是看不懂为什么当年要这么设计。依据在代码之外。可能是服务商文档某个角落的限制,可能是客户生产环境踩过的坑,或者只是上线前一天临时改的一个参数。当时所有人都知道为什么,三个月后没人记得。47 那个例子就属于这一类。文档写每秒 50 次请求,但限流器按 1.2 秒窗口衡量突发,50 次重试会触发限制。所以只能设 47。这个 47 不是代码能看见的,是文档和实际行为之间那 0.2 秒窗口逼出来的。代码里写 47 或 50 都像是对的,但只有 47 不会炸。代码本身没法说“50 不对”。

争议就在这。Clean Code 这个流派被很多人读成一套洁癖规则:注释越少越高级。一个“干净”的代码库最好一行注释都没有。这个标准本身就是偷懒。它把坏注释和注释混为一谈,然后拿坏注释当证据,把整个品类打成坏味道。

坏注释当然存在。帖子里也列了:echo 注释、没有信息的 XML 文档、注释掉的旧代码、和代码矛盾的注释、没有明确条件的 TODO。这些都坏。但坏的是内容,不是注释这个媒介。见过烂函数,没人说“函数是坏味道”;见过烂命名,没人说“命名是坏味道”。烂注释一抓一把,大家就说“注释是坏味道”。这个逻辑弱,而且带着一种道德快感:删掉一段注释,好像自己也高级了。这跟代码质量没什么关系。真正该讨论的是“这条知识如果不在注释里,它还能存在于哪里”。

有人会接一句:理由可以放在 commit message 里。作者专门反驳了这个,我还想再补几刀,因为这套话听起来专业,其实是典型的知识成本转嫁。commit message 擅长回答“这次变更改了什么”,不擅长回答“当前状态为什么这样”。一年以上的文件,git log 往往是一堆无语义的混合:apply editorconfig、fix、fix again、PR feedback、final fix。这些信息在提交时也许对当下有用,但跟当前代码状态没有稳定对应关系。要追某一行为什么是 47,得从 log 里翻十几条,还可能发现真正的原因在一个 squash 合并里被拍扁了。blame 会衰减。重构、重命名、移动文件、squash,都会把 blame 这条线磨断。更关键的是,人对可疑的代码会跑 blame,对奇怪的代码会跑 blame,但对一段看起来没问题的代码,往往等改坏之后才想追。而等改坏的时候,写下 47 的人可能已经离职了。知识跟着人走,仓库里什么也没留下。

注释和 commit message 的区别是:一个工作在当前行旁边,一个工作在历史里。历史像储物间,所有东西都说自己在那里,但翻起来要半天。注释是贴在设备旁那张“先关这个阀再开那个阀”的纸。拿 commit message 去替代注释,是让未来的人做考古。写代码的人省几十秒,维护的人多花几十分钟。谁买单,谁接刀。

监管场景只是把这件事放大了。有评论说审计员或合规审查者没有仓库访问权。他们不看 git blame,也不该花时间考古。他们要看的是需求如何落到实现上。一段注释能把服务商文档里的 50 和代码里的 47 连起来。这不仅是解释,还是在替团队应付“谁批准的”这种问题。更现实的是,那个 47 可能被某个自信但错误的人“修”成 50,因为文档写的是 50。这种人我不止一次见过。不是被恶意改坏,是被善意修好。没有注释的约束,47 在他们眼里就是个显而易见的 bug,等它被改成 50,线上限流就开始抽风。

到这里,真正的问题已经不是“注释该不该写”,而是“谁该为消失的决策上下文付钱”。一个没有注释的代码库里,上下文存在于少数人的脑子里。人离开时,成本不会消失,会变成慢查询、线上事故、新人问不出来但也没人回答的沉默。代码还是那些代码,但库已经从一个人维护的系统,变成一个只能照着改的遗物。

顺带说一句 DRY 教条化。帖子提到一个 ProcessOrder 调用,参数列表是 true、false、null、customer、3、legacy、skipValidation 这种。规则被教条化就是这样。DRY 本来合理,被当成正义之后,就会抽出一些奇怪的共享函数,参数越加越多,每个调用点比原来的重复代码还难读。但因为“重复是坏味道”已经变成了机械反应,没人敢说这个抽象更糟。注释也是这么被对待的。因为“注释是坏味道”已经变成了机械反应,所以没人认真讨论这条决策知识如果不在注释里,它到底该在哪儿。

这段算得有点碎,但账不细算不清。

我的判断摆在这儿:“代码自解释”是个有条件的工程理想,不是无条件的美德。它只在问题结构简单而稳定、且所有约束都能被代码结构表达时才成立。真实项目里,这个条件经常不满足。注释真正的分界线,不在“多还是少”,在“复述还是推理”。复述代码行为的注释是垃圾,没有例外。解释为什么是 47 不是 50 的注释,是现场资产,是代码库当前状态的一部分。把这两者一起删掉,是错杀。把理由藏进 git log,是成本转嫁。

这个判断我会在两种条件下改口。哪一天代码浏览工具能在任意一行旁边自动重建出它存在的原因和当初的约束,而且检索成本低于写和维护注释,我今天这套话就作废。或者,如果一个团队里的代码平均寿命短到上下文丢失几乎没有任何代价,这个判断也可以推翻。到目前为止,这两个条件我一个都没见着成立的迹象。

把 47 上面那几行字删掉很容易。三年后的故障复盘里,那个问题才真正贵。

深潜
深潜

把一个行业趋势拆到商业+技术+利益格局,最后给一句明确判断。

查看主页 →