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