第59章:代码坏味道:注释 – 实战用代码表达意图,移除冗余注释

说实话,我见过不少团队把「写注释」当成KPI来考核。结果呢?代码里到处都是废话注释,真正该解释的地方反而一片空白。这章我们就来聊聊,怎么用代码本身说话,把那些多余的注释干掉。

1. 注释的「好」与「坏」

先别急着删注释。注释本身不是敌人,冗余的、过时的、误导性的注释才是。我个人习惯把注释分成三类:

类型 例子 评价
废话注释 // 设置用户名为张三 ❌ 代码已经说清楚了
意图注释 // 这里用双重校验锁,防止并发下重复初始化 ✅ 解释了「为什么」
TODO注释 // TODO: 后续需要接入统一认证 ⚠️ 临时标记,需定期清理

我在项目中遇到过最离谱的注释,是那种跟代码逻辑完全对不上的。比如代码明明改了,注释还停留在三年前的版本。你想想看,这种注释比没有更可怕——它会误导人。

2. 用代码表达意图,而不是用注释解释

核心原则就一句话:好代码本身就是最好的注释。怎么做到?我总结了三个实战技巧。

2.1 提取方法,用函数名说话

先看一段「反面教材」:

// 检查用户是否有权限访问该资源
// 如果用户是管理员,直接返回true
// 否则检查用户角色是否在允许的角色列表中
if (user.role === 'admin' || allowedRoles.includes(user.role)) {
  return true;
}
return false;

这段代码的注释比代码还长。重构后:

function hasAccessToResource(user, allowedRoles) {
  return user.isAdmin() || allowedRoles.includes(user.role);
}

你看,函数名 hasAccessToResource 已经把意图表达清楚了。注释?不需要了。

我的习惯: 如果一段代码需要写注释来解释「它在做什么」,那就应该把它提取成一个独立函数,用函数名代替注释。

2.2 引入变量,消除魔法数字

魔法数字是注释的重灾区。比如:

// 超时时间设为30秒,防止用户等待太久
setTimeout(callback, 30000);

重构后:

const TIMEOUT_DURATION_MS = 30 * 1000; // 30秒
setTimeout(callback, TIMEOUT_DURATION_MS);

嗯,这里要注意。常量名本身已经说明了含义,但如果你觉得还不够,可以保留一条简短注释解释「为什么是30秒」。但千万别写成「设置超时时间为30秒」这种废话。

2.3 用断言代替条件注释

有些注释是用来提醒调用方注意前置条件的。比如:

// 注意:传入的list不能为null
public void process(List<String> list) {
  // ...
}

更好的做法是用断言:

public void process(List<String> list) {
  Objects.requireNonNull(list, "list must not be null");
  // ...
}

这样,注释变成了可执行的契约。一旦违反,程序会立刻报错,而不是等到运行时莫名其妙地崩溃。

3. 哪些注释可以保留?

说了这么多要删的,那哪些注释值得保留呢?我个人的经验是:

  • 解释「为什么」的注释:比如「这里用HashMap而不是TreeMap,因为查询频率远高于排序需求」
  • 记录历史决策的注释:比如「这个字段不能加索引,因为之前上线后导致写入性能下降80%」
  • 引用外部文档的注释:比如「参考RFC 7231第4.3节关于缓存控制的定义」
我曾经踩过的坑: 有一次我删掉了一条看起来像废话的注释,结果那是业务方特意留下的「特殊逻辑说明」。从那以后,我删注释前都会先确认一下:这条注释是不是在解释「为什么这么做」,而不是「做了什么」。

4. 实战案例:一步步移除冗余注释

来看一个完整的例子。这是我从一个老项目中摘出来的真实代码(做了脱敏处理):

// 计算订单总价
// 1. 遍历所有商品
// 2. 累加每个商品的价格
// 3. 如果总价超过100,打9折
// 4. 返回最终价格
public double calculateTotal(List<Item> items) {
  double total = 0.0;
  // 遍历商品
  for (Item item : items) {
    // 累加价格
    total += item.getPrice();
  }
  // 判断是否打折
  if (total > 100) {
    // 打9折
    total = total * 0.9;
  }
  // 返回结果
  return total;
}

这段代码的注释几乎每一行都是废话。重构后:

public double calculateTotal(List<Item> items) {
  double subtotal = sumItemPrices(items);
  return applyDiscountIfEligible(subtotal);
}

private double sumItemPrices(List<Item> items) {
  return items.stream()
              .mapToDouble(Item::getPrice)
              .sum();
}

private double applyDiscountIfEligible(double amount) {
  double DISCOUNT_THRESHOLD = 100.0;
  double DISCOUNT_RATE = 0.9;
  return amount > DISCOUNT_THRESHOLD ? amount * DISCOUNT_RATE : amount;
}

你看,代码量没少多少,但可读性提升了不止一个档次。每个函数只做一件事,函数名就是最好的注释。

5. 知识体系:注释重构的核心逻辑

下面这张图总结了我对注释重构的理解:

注释重构决策树 遇到一条注释 解释「是什么」 解释「为什么」 尝试用代码表达 (提取方法/引入变量) ❌ 删除注释 保留注释 (可适当精简措辞) ✅ 保留注释 核心原则:好代码本身就是最好的注释

6. 避坑指南

最后分享几个我踩过的坑:

  • 别一刀切删光所有注释。有些注释是业务方留下的「保命符」,删之前先搞清楚上下文。
  • 注意注释和代码的同步。如果改了代码没改注释,那注释就成了毒药。我建议每次Code Review时,顺便检查一下注释是否还准确。
  • 不要用注释来「道歉」。比如「这段代码很烂,先这样吧」——与其写这种注释,不如花10分钟把代码重构好。

说白了,注释重构的核心就一句话:让代码自己说话。当你发现需要写注释来解释代码时,先停下来想一想——能不能通过重构让代码更清晰?如果能,那就动手改代码;如果不能,再写注释也不迟。


公众号:蓝海资料掘金营,微信deep3321