跳到正文

注释写的是为什么 ​

代码回答「做了什么」,注释回答「为什么这样做」。分不清这两件事,注释就会变成重复代码的噪音。

注释的职责 ​

读懂一段代码通常不难,难的是判断它能不能改、能不能删、为什么必须长成这样。这些信息不在语法里,只能由写代码的人留下来。

判断标准很简单:如果删掉这条注释,读者从代码里得不到任何损失,那它就没有存在价值。反之,一条注释如果能阻止一次错误的重构,它就值回维护成本。注释密度跟着代码的晦涩程度走,一目了然的地方不需要旁白。

读者的问题代码能回答需要注释
这段代码做什么能通常不必
为什么用这个结构不能需要
这个常量从哪来不能需要
为什么不用更直接的写法不能需要
谁调用了这个函数工具能不必

值得写的几类 ​

  • TODO:写清未完成的部分、推迟的理由、什么条件下必须做
  • 引用:论文、RFC、上游实现,用永久链接而不是会漂移的行号,并注明与参考实现的差异
  • 正确性论证:代码给出步骤,注释解释为什么这些步骤成立
  • 踩坑记录:调了半小时才找到的修复,后来人不会自己想到
  • 常量来源:1492 是协议字段、压测结果,还是随手选的
  • 承重细节:正确性依赖迭代顺序这类隐含前提,必须写明
  • 「为什么不」:主动放弃的方案,否则会被后人「修好」
python
# 必须用有序结构:下游按插入顺序生成变更集,
# 换成 set 会让每次 diff 变成全量重写(见 #4821)。
seen: dict[str, int] = {}

# 1492 是服务端协议固定的版本号,不是可调的常量
PROTOCOL_VERSION = 1492

第一段注释解释了选择,第二段注释阻止了一次看似合理的「重构」。这两种信息都无法从代码本身推出来。

反例 ​

python
# 把计数加一
count += 1

# TODO: 优化

第一条只是把代码翻译成自然语言,改代码时还要记得同步改它。第二条没有说明优化什么、为什么现在不优化,接手的人只能猜。

更糟的是过时注释。代码已经改了,注释还停在旧行为上,读者会先相信注释,再被代码打脸。评审时看到与代码不符的注释,应当直接标为缺陷,而不是顺手放过。

让注释活得久 ​

改代码时把注释当成同一个变更的一部分。提交前扫一遍 diff 里的注释,确认它描述的行为仍然成立。

  • git blame 看某条注释最后一次被修改的提交
  • git log -S'PROTOCOL_VERSION' 找常量被引入的提交
  • 用 TODO(name) 标明负责人
  • 定期 rg 'TODO|FIXME|HACK' 清理
  • 团队内统一 XXX、HACK、FIXME 的严重程度

有些团队把 FIXME 当作必须修、TODO 当作计划内、HACK 当作已知妥协,搜索时就能按严重程度过滤。约定本身不重要,一致才重要;各写各的标记,等于没有标记。

注释也不该用来道歉或抱怨。写「这里写得丑」对读者没有帮助,写清丑的原因和替换条件才有。

注释的形式 ​

行内注释解释紧邻的那一行,段落注释放在代码块之前,函数注释描述契约而不是实现步骤。位置错了,再好的内容也会被忽略。

  • 文档注释进 API 文档:Python 的 docstring、Rust 的 ///、Go 的包注释
  • 实现注释留在函数体内,解释为什么这样写
  • 变更记录交给提交信息,不要堆在文件头
  • 作者、日期这类信息由版本控制提供,不必手写

工具会读取文档注释生成参考手册,所以那里写行为、参数和错误,不写内部循环怎么转。实现注释可以随意具体,它的读者是正在改这段代码的人。

同一个仓库里,文档注释和实现注释的语气也要区分。前者面向调用者,用陈述句写清保证和限制;后者面向修改者,可以写猜测、临时方案和未验证的假设,只要标明状态。