一次批量重构后,助手给 40 个方法补了注释,风格统一、措辞得体。而其中 6 个方法的注释与实现不符——因为它读的是重构前的语义,而重构改变了行为。三个月后,一位同事按注释理解代码,改错了一个分支。
注释的价值与代码相反:代码越新越可信,注释越旧越可疑。这个错位的机制很清楚:模型能在几秒内生成 40 条"看起来很完整"的注释,而它读的语义是"生成那一刻的代码"——之后代码再改,注释不会跟着变。
而"让模型生成注释"这件事,恰好会批量生产"看起来很新、实际可能过期"的注释。
1. 三种注释的寿命
| 类型 | 寿命 |
|---|---|
| 说明"做什么" | 短(代码一改就错) |
| 说明"为什么" | 长(原因通常不变) |
| 说明"约束/前提" | 长(前提变了才会变) |
第一项是"最容易过期"的一类:它复述代码,而代码改一行它就可能错。而模型最擅长的恰恰是写这一类。
第二项与第三项才是"有长期价值"的:**"为什么用 8 秒超时"、"这里假设上游不返回 null"**第二项的"原因通常不变"有一个例外:当"约束变了",原因就过期了。比如"因为上游超时是 5 秒,所以这里设 8 秒"——上游改成 20 秒后,这条注释从"正确"变成"误导"。
—— 这些信息在代码里看不出来,而它们不随实现细节变化。
2. 让模型写注释的风险分布
| 注释类型 | 生成质量 | 过期风险 |
|---|---|---|
| 接口的用途说明 | 高 | 低 |
| 参数/返回值说明 | 高 | 中 |
| "为什么这样写" | 低(它不知道原因) | — |
| 复杂算法的步骤 | 中 | 中 |
第二项(参数说明)的生成质量高,是因为信息全在签名里。而它也有"细节漂移":参数含义被收窄(原来允许 null,现在不允许)时,描述不会自动更新。
第三项是硬约束:模型不知道"为什么",只能从代码倒推,而倒推出来的"原因"往往是错的(且写得很像真的)。
第四项在"它读懂了算法"时质量不错,但**它可能把"实现方式"当成"目的"**第四项还有个"过度解释":把三行代码解释成一段文字,看起来详尽,实际让读者读得更慢——因为"读注释"与"读代码"是两件事,都要花时间。
—— 比如把"用二分查找"写成"为了性能",而真实原因可能是"数据有序且需要早停"。
<!-- 反例:模型倒推的"为什么"(看起来很合理,实际是猜的) -->
// 使用二分查找以提高性能
<!-- 真实原因(只有作者知道) -->
// 上游按时间升序推送,且我们只需要最近一条,所以从尾部二分
3. 什么样的注释值得让模型写
| 场景 | 是否适合 |
|---|---|
| 公开 API 的用途与参数 | 适合 |
| 数据结构的字段含义 | 适合 |
| 复杂表达式的拆解 | 适合 |
| 业务规则的理由 | 不适合 |
| 历史决策与替代方案 | 不适合 |
第四项(业务规则)还有一个"边界"问题:规则本身可能有例外("满 100 减 15,但生鲜不参加"),而模型只会写出它能看到的规则,写不出例外——除非代码里恰好有。
第一项最适合:"这个方法做什么、参数是什么、返回什么"——信息全在签名与实现里,模型能准确提取。
第四项必须人工:"满 100 减 15"这条规则为什么是 15 而不是 20,只有业务方知道。
4. 手工补"为什么"的三个模板
| 模板 | 例子 |
|---|---|
| 因为 X,所以 Y | 因为上游可能返回空数组,所以这里判空 |
| 若改成 Z 会怎样 | 若改成非阻塞,会在高并发下丢事件 |
| 前提是 W | 前提是 id 全局唯一(由 DB 序列保证) |
第一项最实用:它把"代码里看不到的原因"写下来,而这正是最有价值的注释。
第二项是"防误改":"若改成 X 会崩"比"这里必须这样写"更有说服力第三项(前提)最容易被漏:前提是隐式的("id 全局唯一"由 DB 序列保证),而它一旦不成立,代码就错了。这类注释的价值在于"让前提可被检查"。
,因为它给出了因果。
// 好注释(三行,含原因与前提)
// 因为上游在超时时会返回空数组而不是抛异常,所以这里用 isEmpty 判断而不是 catch。
// 若改成"依赖异常",超时会被当成"没有数据",导致静默少算。
// 前提:上游版本 >= 2.3(旧版本超时会抛)。
"删除"其实是最有效的注释维护动作,而没人愿意做。可以定一条规则:改动一处代码时,若它的注释"做什么"与新的实现不符,就删掉那条注释(而不是改它)——因为"改注释"往往只是把它改成新的重复。
// 三种处置(按优先级)
1 删:只是复述代码的注释(改了代码就删,别改它)
2 留:说明"为什么"的注释(改代码时检查原因是否还成立)
3 改:说明"前提/约束"的注释(前提变了必须改,并标注日期)
5. 注释的"过期检测"
| 手段 | 效果 |
|---|---|
| 把注释当断言(示例代码可编译) | 强 |
| 文档测试(doc test) | 中(限 Python 等) |
| 定期人工抽查 | 弱但必要 |
| 只保留"为什么"类注释 | 从源头减少 |
第一项的代价是"维护成本":示例里的 API 改名要同步改,否则编译失败。而它恰好是目的——用编译失败来提醒"注释需要更新"。
第一项最有效:注释里的示例代码如果可编译/可运行,它就不会过期——因为过期的示例编译不过。
第四项是"结构性"解法:如果只写"为什么",那"代码改了注释要不要改"这个问题就很少出现(因为原因通常不变)。
/// 示例(可被 doclint 或测试校验)
/// <pre>{@code
/// var n = Parser.count("a,b,c"); // 3
/// }</pre>
// 若方法签名变了,这段示例编译失败 → 注释被迫更新
"注释与代码的一致性"还有一层"风格"问题:同一个项目里有的注释用中文、有的用英文,而模型会照它"看到的那一片"的风格生成——于是同一批生成里出现两种语言。约定应当写进提示,或写进仓库的 lint 规则。
// 生成前的两条约定(写进提示)
- 注释语言:中文(标识符与 API 名保留英文)
- 不写"做了什么"类注释;只写"为什么/前提/坑"
// 若无约定,模型会混合风格,评审时又不好统一要求
6. 排错:五种"注释反而误导"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 按注释改错代码 | 注释过期 | 删掉"做什么"类注释 |
| 注释与实现不一致 | 批量生成未复核 | 只生成可核查的部分 |
| 注释解释"为什么"但错了 | 模型倒推 | 人工补原因 |
| 注释太多没人读 | 复述代码 | 精简到"为什么" |
| 新人信注释不信代码 | 注释太"完整" | 标注"以代码为准" |
第五项最危险:一份"看起来完整"的注释比"没有注释"更糟第二项(批量生成未复核)的可操作防线是"只让模型生成"可核查"的部分":接口用途、参数、字段含义——这些能对照签名验证;而"为什么"一律人工。
,因为它让人放弃阅读代码。
最后一个"成本"视角:注释不是免费的,它会随代码改动产生维护负担(即使只是"读一遍确认还对不对")。所以"多写注释"不等于"好维护"——有用的注释是少数几条"为什么",其余应当是"命名"与"结构"的责任。
// 三个替代注释的手段(优先于写注释)
1 改名字:把"为什么"编码进方法名(如 retryOnTimeout)
2 拆函数:把前提写成前置检查(if (id == null) throw ...)
3 加断言:把约束变成运行时检查(require(unique))
// 三者都比注释更强,因为它们会被执行
7. 边界:什么情况不必写注释
| 场景 | 为什么 | 做法 |
|---|---|---|
| 自解释代码(命名清晰) | 注释是重复 | 不写 |
| 一次性脚本 | 不长期 | 不写 |
| 测试代码 | 用例名即说明 | 少写 |
| 已在文档/ADR 里说明 | 有单一来源 | 只留链接 |
最后一条与"团队"有关的边界:注释的读者是"三个月后的自己"。所以判断标准可以更具体:"三个月后我会不会因为不知道这条信息而踩坑"——会,就写;不会,写了也没人读。
判断口径一句话:这条信息"在代码里能不能看出来"。能(做什么、怎么做),注释是多余的;不能(为什么、前提、若改动会怎样),注释才有价值。
8. 自测
- 三种注释的寿命为何不同?为什么"说明做什么"最容易过期?
- 模型写"为什么"类注释为什么不可靠?它会把什么当成目的?
- 哪三类注释适合让模型写?哪两类必须人工?
- "因为 X 所以 Y"与"若改成 Z 会怎样"分别解决什么问题?为什么后者更有说服力?
- 为什么"示例代码可编译"是有效的过期检测?
- 判断"是否需要注释"的那个问题是什么?