代码回答「做了什么」,注释回答「为什么这样做」。分不清这两件事,注释就会变成重复代码的噪音。
读懂一段代码通常不难,难的是判断它能不能改、能不能删、为什么必须长成这样。这些信息不在语法里,只能由写代码的人留下来。
判断标准很简单:如果删掉这条注释,读者从代码里得不到任何损失,那它就没有存在价值。反之,一条注释如果能阻止一次错误的重构,它就值回维护成本。注释密度跟着代码的晦涩程度走,一目了然的地方不需要旁白。
# 必须用有序结构:下游按插入顺序生成变更集, # 换成 set 会让每次 diff 变成全量重写(见 #4821)。 seen: dict[str, int] = {} # 1492 是服务端协议固定的版本号,不是可调的常量 PROTOCOL_VERSION = 1492
第一段注释解释了选择,第二段注释阻止了一次看似合理的「重构」。这两种信息都无法从代码本身推出来。
# 把计数加一 count += 1 # TODO: 优化
第一条只是把代码翻译成自然语言,改代码时还要记得同步改它。第二条没有说明优化什么、为什么现在不优化,接手的人只能猜。
更糟的是过时注释。代码已经改了,注释还停在旧行为上,读者会先相信注释,再被代码打脸。评审时看到与代码不符的注释,应当直接标为缺陷,而不是顺手放过。
改代码时把注释当成同一个变更的一部分。提交前扫一遍 diff 里的注释,确认它描述的行为仍然成立。
git blame
git log -S'PROTOCOL_VERSION'
TODO(name)
rg 'TODO|FIXME|HACK'
XXX
HACK
FIXME
有些团队把 FIXME 当作必须修、TODO 当作计划内、HACK 当作已知妥协,搜索时就能按严重程度过滤。约定本身不重要,一致才重要;各写各的标记,等于没有标记。
TODO
注释也不该用来道歉或抱怨。写「这里写得丑」对读者没有帮助,写清丑的原因和替换条件才有。
行内注释解释紧邻的那一行,段落注释放在代码块之前,函数注释描述契约而不是实现步骤。位置错了,再好的内容也会被忽略。
///
工具会读取文档注释生成参考手册,所以那里写行为、参数和错误,不写内部循环怎么转。实现注释可以随意具体,它的读者是正在改这段代码的人。
同一个仓库里,文档注释和实现注释的语气也要区分。前者面向调用者,用陈述句写清保证和限制;后者面向修改者,可以写猜测、临时方案和未验证的假设,只要标明状态。
注释写的是为什么
代码回答「做了什么」,注释回答「为什么这样做」。分不清这两件事,注释就会变成重复代码的噪音。
注释的职责
读懂一段代码通常不难,难的是判断它能不能改、能不能删、为什么必须长成这样。这些信息不在语法里,只能由写代码的人留下来。
判断标准很简单:如果删掉这条注释,读者从代码里得不到任何损失,那它就没有存在价值。反之,一条注释如果能阻止一次错误的重构,它就值回维护成本。注释密度跟着代码的晦涩程度走,一目了然的地方不需要旁白。
值得写的几类
第一段注释解释了选择,第二段注释阻止了一次看似合理的「重构」。这两种信息都无法从代码本身推出来。
反例
第一条只是把代码翻译成自然语言,改代码时还要记得同步改它。第二条没有说明优化什么、为什么现在不优化,接手的人只能猜。
更糟的是过时注释。代码已经改了,注释还停在旧行为上,读者会先相信注释,再被代码打脸。评审时看到与代码不符的注释,应当直接标为缺陷,而不是顺手放过。
让注释活得久
改代码时把注释当成同一个变更的一部分。提交前扫一遍 diff 里的注释,确认它描述的行为仍然成立。
git blame看某条注释最后一次被修改的提交git log -S'PROTOCOL_VERSION'找常量被引入的提交TODO(name)标明负责人rg 'TODO|FIXME|HACK'清理XXX、HACK、FIXME的严重程度有些团队把
FIXME当作必须修、TODO当作计划内、HACK当作已知妥协,搜索时就能按严重程度过滤。约定本身不重要,一致才重要;各写各的标记,等于没有标记。注释也不该用来道歉或抱怨。写「这里写得丑」对读者没有帮助,写清丑的原因和替换条件才有。
注释的形式
行内注释解释紧邻的那一行,段落注释放在代码块之前,函数注释描述契约而不是实现步骤。位置错了,再好的内容也会被忽略。
///、Go 的包注释工具会读取文档注释生成参考手册,所以那里写行为、参数和错误,不写内部循环怎么转。实现注释可以随意具体,它的读者是正在改这段代码的人。
同一个仓库里,文档注释和实现注释的语气也要区分。前者面向调用者,用陈述句写清保证和限制;后者面向修改者,可以写猜测、临时方案和未验证的假设,只要标明状态。