7AA7 博客

代码注释应该写什么:什么该写、什么不该写

舒野 UID:75
2026-09-30 20:59
92 0 3

“代码要有充分注释”这句话害了不少项目。它没说清楚”充分”是多少,于是有人给每一行都配一句中文翻译,评审时谁也不敢说不对——毕竟注释多一点总没坏处。

有坏处。注释和代码一样是要维护的资产,而且它比代码更容易腐化:代码错了测试会挂,注释错了没人知道。

注释的唯一价值

注释能提供的价值只有一个:补充代码本身无法表达的信息。

代码能表达”做了什么”——读一遍就知道了。代码表达不了的是:

  • 为什么这么做(业务背景、历史原因、取舍依据)
  • 为什么不那么做(看起来更明显的方案为什么被排除)
  • 隐含的约束前提(调用方必须保证什么、外部系统的怪癖)

除了这三类,其他内容都应该考虑用代码本身表达,或者直接不写。

不该写的三类

一、复述代码。

// 设置用户名为 Tom
user.setName("Tom");

// 循环遍历订单列表
for (Order order : orders) {
    // 如果订单金额大于 100
    if (order.getAmount() > 100) {
        // 打印订单号
        System.out.println(order.getId());
    }
}

这类注释的信息量等于零,还带来维护成本——代码改成 setName("Jerry") 而注释没改,就变成了误导。更糟的是,它给读者一种”这段代码被解释过了”的错觉,反而降低了阅读时的警惕性。

二、过时的信息。 曾经正确但已经失效的描述,比没有注释更危险。

// TODO: 等 v2 接口上线后替换为 ApiV2Client
// 注意:此方法不支持并发调用,需要外部加锁
public List<Item> fetchItems() { ... }

第一行如果 v2 已经上线两年了,就是噪音。第二行如果方法内部已经加了锁,就是在误导。

三、显而易见的文档注释。 为了凑覆盖率而生成的注释,常见于 IDE 自动生成或 AI 补全:

/**
 * 获取用户
 * @param id 用户ID
 * @return 用户
 */
public User getUser(Long id) { ... }

这三行里没有任何一行是读代码看不出来的。

好注释的四种类型

意图说明

解释”为什么是这段代码”,尤其是看起来有点怪的地方。

// 这里必须用 200 而不是 204:部分老版本客户端在收到 204 时会
// 认为响应体丢失并触发重试,导致重复下单。
// 详见 issue #4821。
w.WriteHeader(http.StatusOK)
# 先写 outbox 再提交事务,不能反过来。
# 反过来的话,进程在两步之间崩溃会产生"业务未生效但事件已发出"的幽灵消息。
outbox.insert(event)
db.commit()

约束警告

说明调用方或后续维护者必须遵守的前提。

/**
 * 非线程安全。如果需要并发访问,调用方必须自行同步,
 * 或者使用 {@link ConcurrentPriceCache}。
 */
public class PriceCache { ... }
// 调用方必须保证 key 不为 nil,否则会 panic。
// 历史原因:早期版本允许 nil 表示"全部",后来语义改了但保持 panic 以便快速暴露问题。
func Lookup(key string) ([]byte, error) { ... }

领域知识

代码里那些”魔法数字”和非常规流程,通常有业务来源。

// 90 天不是随便定的:该品类商品的最长退换货周期是 88 天,
// 留 2 天缓冲用于对账延迟。改这个值前先跟售后团队确认。
private static final int ARCHIVE_AFTER_DAYS = 90;
// 费率按"分"计算并向下取整,这是财务口径要求的,不能改成四舍五入。
// 差异部分在月度结算时统一处理。
func calcFee(amountCents int64, rate float64) int64 {
    return int64(float64(amountCents) * rate)
}

TODO 的规范写法

TODO 不是”以后再说”的垃圾桶,它需要足够的信息让别人能接着做。

// TODO(zhang): 2026-11 之前替换为批量接口,当前 N+1 查询在大促时 P99 达到 800ms。
// 阻塞原因:依赖 inventory-svc v3.2 发布。
// 参考:RFC-0173

一条合格的 TODO 至少要说清三件事:做什么、为什么现在没做、谁负责。没有负责人和期限的 TODO,半年后没人敢删也没人敢做。团队里可以定个规矩:TODO 必须带工单号,超过半年自动转成技术债清单。

用命名与结构减少注释需求

很多注释的存在,是因为代码没写好。

用变量名替代注释:

// 如果账户处于冻结状态则拒绝提现
if (acct.getStatus() == 3 && acct.getBalance() > 0) { reject(); }

// 改为
if (acct.isFrozen() && acct.hasBalance()) { reject(); }

用提取方法替代注释:

// 计算可提现金额(余额减去冻结金额和手续费)
long a = balance - frozen - (balance - frozen) * feeRate;

// 改为
long withdrawable = calculateWithdrawable(balance, frozen, feeRate);

用常量替代注释:

if len(items) > 500 { batch = true }  // 超过 500 走批量

// 改为
const batchThreshold = 500
if len(items) > batchThreshold { batch = true }

判断标准:如果你要给一段代码写注释,先问能不能用命名、提取、常量让它自己说清楚。 能,就改代码;不能,才写注释。

注释会腐化,怎么维护

注释腐化的根源是它和代码之间没有任何强制关联。代码改了,注释不会报错。

几个可行的约束:

一、评审时把注释当作代码的一部分检查。 修改函数逻辑时,diff 里如果只有代码没有注释变更,评审者要问一句”上面的注释还成立吗”。这是最有效也最依赖人的一条。

二、让注释靠近它描述的代码。 注释离代码越远,被遗忘的概率越高。文件头的”本文件包含 XX 功能”这种注释,几乎必然过时。

三、优先写”为什么”而不是”是什么”。 “为什么”变化慢,”是什么”变化快。上面那些例子里,90 天的业务原因、200 状态码的历史包袱,比”循环遍历列表”稳定得多。

四、定期清理 TODO。 把超过期限的 TODO 扫一遍,能做的做,不能做的删掉或者转成正式的技术债记录。

五、用测试固化约束。 有些注释描述的是约束(”必须按 ID 排序”),如果这种约束能用测试断言表达,就比注释可靠得多。

// 不需要写"返回的 items 必须按 createTime 升序"
// 直接写测试
func TestListItemsSortedByCreateTime(t *testing.T) { ... }

文档注释与实现注释的分工

两类注释的读者不同,内容也不同。

文档注释(API doc) 实现注释
读者 使用者(不读实现) 维护者(正在读实现)
内容 做什么、契约、边界、异常 为什么这么做、约束、背景
位置 公开接口上方 函数内部、关键语句旁
变化频率 低(接口变更才改) 中(实现变更要同步)
举例 “金额单位:分;余额不足抛 InsufficientError” “单位用分是为了避开浮点误差,财务口径要求”

文档注释的检验标准:使用者只看这段注释,能不能正确调用? 参数单位、空值语义、时间复杂度、可能抛出的错误、是否线程安全——这些不说清楚,使用者就得去读源码。

实现注释的检验标准:半年后的自己看到这段注释,能不能理解当初的取舍? 如果注释只是”这里做了 XX”,那它没有存在的价值。

反面例子对照

反例 问题 改法
// 增加计数 配 count++ 复述代码 删掉
// 修复了 bug 无信息量 写清 bug 成因和工单号
// 这段代码很复杂,小心修改 恐吓但不解释 写清复杂在哪、约束是什么
/** 获取用户 */ getUser(Long id) 显而易见 补充契约:不存在时返回 null 还是抛异常
注释描述 A,代码做 B 过时误导 改代码时同步改注释
// 临时方案 无期限无人名 永不清理 补负责人、期限、工单号
大段注释掉的代码块 噪音 删掉,版本控制里有历史

那行”这段代码很复杂,小心修改”尤其典型。它传达的情绪是真实的,但信息是零——读者依然不知道雷在哪。改成”这里的排序依赖上游保证的 ID 单调性,上游改造前不能改成并发处理”,才是有效信息。

一条可执行的判断

写注释前问自己:这段信息,读代码的人能从代码里得到吗?

能,就别写,先想办法让代码更清楚。不能,才写——并且写的是”为什么”,不是”是什么”。

这条规则同时解释了好注释和坏注释:解释”为什么用分而不是元”的注释有价值,因为代码里只有一个 long;解释”循环遍历订单”的注释没价值,因为 for (Order o : orders) 已经说得很明白了。

1.7k浏览量
92赞
0评论
0参与用户
11 分钟阅读时间

评论(0)

还没有评论,来抢沙发。