先摆个立场:那标题确实是我写的,但我不是让你真的把项目里所有注释一夜之间全删干净。我真正想说的是,绝大多数注释根本不该存在——它们不是帮助,是债,是坑,是你未来某天半夜调bug时突然背上的刺。
我前阵子接手一个老项目,一个方法大概两百行,里面密密麻麻三十多条注释。你以为这是好事?结果其中十一条跟代码实际行为对不上,五条描述的是已经被删掉的逻辑。我照着注释去理解,理解出来一个根本不存在的业务,最后定位问题多花了整整一下午。那一下午我脑子里只有一句话:注释这东西,写的时候是天使,读的时候全是恶魔。
为什么会有“kegg注释”这种词挂在热搜上?因为“注释”这个词在不同领域含义差得很远,生物学里的序列注释和程序员代码里的注释完全不是一回事。但核心逻辑其实是通用的:注释这个过程意味着给一段东西附加解释信息,而这个解释一旦产生,就开始面临一个永恒的问题——它会不会和原始内容脱节。
代码注释尤其如此。代码是活的,会跑,会被改,注释是死的,只会躺着。代码更新了,注释还停在十年前;别人踩过你的坑改了逻辑,注释还在描述那个已经不存在的旧流程。所以这篇文章我不谈道德,只谈成本和收益。我会把“什么时候别写注释”“什么时候必须写注释”“怎么用代码本身说话”这三件事讲透,配合真实代码示例和我在项目里踩出来的坑。
不管你是刚入行的新手还是带团队的老兵,这篇文章都值得看完——尤其如果你习惯写完代码顺手补一堆“解释性”注释,那很可能会改变你之后写代码的习惯。
1. 为什么注释会成为“恶魔”:三个真实代价
1.1 注释和代码脱节,形成虚假的安全感
先说最要命的:注释和代码之间没有编译期约束,没有类型检查,没有任何机制能保证注释描述的和代码做的一致。代码错了会报错、会崩溃、会报警,注释错了什么都不发生,它就那么安安静静躺着,等你某天真信了它,然后掉进坑里。
我见过一个真实的例子。一个支付相关的函数,注释写着“此处已处理优惠券抵扣,金额为折扣后价格”,结果后面做活动,产品把折扣逻辑整个移到了上层,这个函数早就变成只算原价了。注释没人改,代码也没人管,直到财务对账发现金额老是对不上。排查两天,真相大白的时候,所有人看那行注释的眼神都不对劲了。
这不是个例,是常态。只要注释存在,它就会给你一种“代码应该没错”的心理暗示——因为注释清清楚楚写着这里做了什么事情。这种虚假的安全感比没有注释更可怕,因为它让你不敢深挖、懒得验证,最终让问题在眼皮底下发酵。
注意:注释不是代码的一部分,它只是代码的“旁白”。旁白说错了,剧本还是照样演。
1.2 维护成本翻倍:一个改动,两处维护
如果你负责过一个活得比较久的项目,你肯定有这种经历:一个需求改动逻辑代码只要五分钟,但同步修改相关注释得再来十分钟,而且还总是忘了改干净哪条。
这就是注释最直接的财务账:本来维护一份表达就够了,现在要维护两份。而且这两份表达之间没有稳定性可言——改了代码忘改注释,是不小心;改了代码故意不改注释,是心累;最后注释和代码的偏差积累到一定程度,整个文件的可读性就会断崖式下降。
更隐蔽的是,注释还会占用你的大脑缓存。你读一个方法的时候,每遇到一条注释就要切换一次思维模式:先读注释在说什么,再读代码在做什么,再对比两者是否一致。这比直接读代码本身消耗的算力高得多。一个本可以用五十行清晰代码讲明白的逻辑,套上二十条注释以后,反而更难看懂了。
1.3 注释是代码坏味道的“遮羞布”
我后来看代码有个习惯:哪个方法注释越多,越可疑。因为真正清爽的代码,本身就容易读懂,你根本不需要在它旁边絮絮叨叨。而当一个人写了一堆注释来解释他的代码,往往意味着他的代码还不够好——变量名叫a、b、c,函数两百行一坨,逻辑埋在三个if嵌套里。
注释在这里起的作用,不是帮助,而是掩饰。它把“这段代码很烂”这件事包装成了“这段代码很复杂,所以需要解释”。你读完注释,觉得写的人挺认真,于是对背后的乱逻辑多了一份容忍。而这份容忍,就是劣质代码存活的土壤。
我见过最夸张的一个例子,一个十几行的函数写了八条注释来介绍每一步在干嘛,但函数名本身叫processData,入参是个object,返回也是个object,全程没人知道data是什么、processed成什么样。那些注释翻译成人话就是:“我处理了一下数据,具体处理成什么样你们看代码吧。”但代码比注释还难看懂。这种注释就是典型的遮羞布——它制造了一个“这段逻辑被解释过”的假象,实际上什么都没说清楚。
2. 哪些注释最坑人:四种常见的坏注释类型
2.1 直译型注释:纯粹噪音
先看最简单的类型。这类注释就是把代码翻译成人话再说一遍,不提供任何增量信息:
// 将i增加1 i++; // 将用户列表按创建时间排序 users.sort((a, b) -> a.createTime.compareTo(b.createTime)); // 设置用户的邮箱 user.setEmail(email);这些注释错了吗?没有。有用吗?一点用都没有。i++谁看不懂?sort按什么排序看下参数不就知道了?你写这种注释的唯一效果,就是让阅读者多扫一眼,浪费一点点脑力,然后把注意力从代码本身挪到一个无用信息上。
如果全项目都是这种注释,它们还会形成一种“噪音污染”。就像你在看一幅画,画上每个颜色旁边都贴了个标签写着“红色”“蓝色”“绿色”,不但不帮助你欣赏,反而让你烦。删掉这类注释是成本最低、收益最明显的事情,直接删就行,不用有任何心理负担。
2.2 过时型注释和误导型注释:最危险
这类型上面已经提到过。过时型注释的可怕之处在于,它错得很安静。代码不会因为你改了逻辑就自动更新注释,注释也不会因为你忘了更新就变红报警。它就在那儿躺着,和代码并存,直到你把它当真理。
举一个我在真实项目里遇到的例子:
# 这里过滤掉停用用户,因为停用用户不能再登录 if user.is_deleted: return None看着是不是挺正常?结果后来业务变化,允许“已删除用户”在 30 天冷静期内重新激活账号,于是代码改成:
if user.is_deleted and not user.can_reactivate(): return None但注释没人动。半年后新同事接手,看到那行注释,振振有词地跟产品说这个接口不可能返回已删除用户——因为他“读过了代码”。这件事最后上升到客诉。你说这行注释算不算恶魔?它太算了。
2.3 注释掉的代码:版本库里的“僵尸”
“先注释掉,不删,万一以后用得上”是我最不理解的程序员习惯之一。代码是干吗用的?是跑起来产生价值的。你注释掉一段代码,它既不跑也没价值,还占着地方吓人。而且版本管理工具就是干这个的——真要用得上,Git 历史里翻得出来,没必要在源码里留僵尸。
注释掉的代码比普通注释坑的地方在于,它们往往会被后来的维护者当成“可选逻辑”而非“死代码”。有人重构时看到一段被注释掉的排序逻辑,以为这是处理某些边界场景的备选方案,就照着这个“备选方案”改了活代码的行为,结果搞出线上bug。这种case我见过不止一次。
我的建议很粗暴:只要确定一段代码当前没在跑,就直接删。担心以后用得上?提交信息里写清楚“删除了XX逻辑”,以后通过git log -S或者git blame都能找回来。
2.4 情绪型和占位型注释:团队协作的隐藏成本
比僵尸代码更隐蔽的是情绪注释和占位注释。
情绪注释长这样:
// 不要动这里!动一次炸一次! // FIXME: 暂时这么写,后面一定重构 // HACK: 这代码我自己都看不懂,但是能用这些注释背后往往藏着一段没人敢动的脆弱逻辑。它的存在不是在帮助后来者理解,而是在传递焦虑。写注释的人早已跑了,留下后面每个人面对那行代码,既不敢改、又看不透、还找不到文档。这种情绪价值,是负的。
占位注释常见于匆忙交付的代码:
// TODO: 这快后面要加权限校验 // 这里应该记录审计日志,主任说下周做注意,我的意思不是否定TODO本身——合理使用下它有提醒价值。但现实是绝大多数的TODO注释会变成永不兑现的承诺。你在代码里放了五条TODO,三个月以后回来一看,一条都没做,它们只是变成了一种心理安慰:“我知道有问题,我已经记录过了。”可这份记录,既不会催你,也没人帮你跟踪,最终就是躺在代码里永不见天日。
实操建议:TODO 可以写,但必须配合任务系统。要么立刻建 task 并关联编号(比如
// TODO(TICKET-2333): 增加重试机制),要么就别写。没有编号的 TODO 等于没写。
3. 让代码自己说话:自文档化的三个落地手法
3.1 命名即文档:把解释塞进名字里
要减少注释,第一件事就是把名字起好。变量名、函数名、类名都是不会被代码逻辑篡改的“活注释”——代码怎么改,名字就跟到哪,不会像旧注释一样停留在上一个版本。
打个比方。下面这段代码有注释:
// 计算用户实际需要支付的金额,会减掉折扣,也可能会加运费 double x = calc(price, discount, shipping);再怎么优化,你还是要靠那条注释去理解calc在算什么。但如果函数本身就叫calculateFinalPrice,入参再清楚一点:
double finalPrice = calculateFinalPrice( originalPrice, applyDiscount(discountRule, originalPrice), estimatedShippingFee(region, weight) );你还需要注释吗?不需要。函数名说清楚了要算什么,参数名说清楚了它拿什么算,每个小函数又解释了折扣和运费各自怎么来。整段逻辑变成了一条可以顺畅读下来的句子。
我自己写代码有个衡量标准:如果一段代码需要注释才能看懂,那优先想办法改名和拆分,而不是直接补注释。命名是比注释便宜得多的文档——它不需要额外维护,因为代码每次编译运行,都是在验证这些名字是否还在正确描述行为。
3.2 把注释的需求“吸”进函数设计
还有一种常见情况:你写注释是因为一个方法里塞了太多逻辑,不解释根本绕不清楚。这种问题的根治办法不是注释,而是拆函数。
看一个经典反例:
def process(orders): # 先过滤已完成订单 valid = [o for o in orders if o.status != "done"] # 再按客户优先级排序 valid.sort(key=lambda o: o.customer.priority, reverse=True) # 前10个走加急 urgent = valid[:10] # 剩下的走普通 normal = valid[10:] # 加急订单要发短信通知 for o in urgent: notify(o.customer, "urgent") # 普通订单发邮件 for o in normal: notify(o.customer, "email") return urgent, normal这里每条注释都在解释一个“本可以叫出名字”的操作。与其用注释解释,不如把操作变成函数,让名字来当注释:
def split_orders(orders): valid = [o for o in orders if o.status != "done"] valid.sort(key=order_priority) return valid[:10], valid[10:] def notify_urgent(orders): for o in urgent: notify(o.customer, "urgent") def notify_normal(orders): for o in normal: notify(o.customer, "email") def process(orders): urgent, normal = split_orders(orders) notify_urgent(urgent) notify_normal(normal) return urgent, normal哪个好维护?一目了然。拆出来的每个函数名本身就是注释的替代品。而且这些“注释替代品”有一个注释永远做不到的优点:它们可以被单独测试、单独复用,出问题时也能单独定位。
我在实践里常用的拆法叫“注释作为拆分清单”:如果一段代码里需要写三条以上注释来描述步骤,我就把这几个步骤分别抽成函数,注释的内容变成函数名。代码重构完,注释几乎会自动消失。
3.3 用类型和数据结构表达意图,而不是解释
第三个利器是类型系统和数据结构设计。很多时候我们想用注释弥补的,是代码本身在表达上的含糊——而这些含糊恰恰可以用类型来解决。
看一个例子:
// 第一个参数是超时秒数,第二个是重试次数 retry(task, 30, 3);这种调用谁看谁懵。但改成对象后:
RetryPolicy policy = RetryPolicy.builder() .timeoutSeconds(30) .maxAttempts(3) .build(); retry(task, policy);不用注释,调用点自己就把话说干净了。再比如,如果你用一个裸布尔值表示“是否是服务端”,读代码的人还得记着这个布尔值的含义。但如果定义一个枚举NodeRole.SERVER、NodeRole.CLIENT,那赋值和比较的时候,语义就长在代码上。
我常说一句话:注释解释的是“代码想干什么”,类型和数据结构限制的是“代码能干什么”。后者显然比前者更可靠——因为违反类型会报编译错误,违反注释什么都不会发生。能用语言本身表达清楚的事情,就不要额外负担一层解释。
4. 必要注释的边界:哪些地方真的该写
光说“别写注释”很容易走火入魔。作为一个认真搞过几年代码库的人,我必须诚实告诉你:确实有一些注释值得写,而且不写才是灾难。关键在于,你要能分辨“什么注释有用”。
4.1 解释“为什么”,而不是“是什么”
写注释唯一不可替代的场景,是代码本身无法表达“为什么这么做”的时候。
“是什么”永远可以用代码自己表达,但“为什么”常常藏在业务背景、历史包袱、外部约束里,这些信息放不进任何函数名或变量名。
举例,这类注释是金子:
// 这里不能用浮点运算,因为金额计算中浮点误差会触发风控误判 const amountInCents = Math.round(price * 100);# 客户端依赖这个响应体字段顺序,不能重排,否则老版本APP会解析崩溃 return {"code": 0, "message": "ok", "data": ...}-- 不用JOIN是因为该表数据量过亿,JOIN会导致查询计划退化,详见性能报告 #4021这些注释解释的都是“代码为什么长成这样”,它背后是大量上下文、失败经验、外部约束。未来的维护者只凭代码,绝对猜不到这里藏着一个雷。所以这条注释不是解释,而是警告,是路标,它值得存在。
4.2 领域知识和业务规则的来源标注
还有一种注释让我感激涕零:把一段看着很奇怪的逻辑,关联到需求的来源。
我处理过一个会员积分规则,代码非常简洁,几行就算完了。但这些规则本身极其反直觉:为什么这个场景积分×1.5?为什么那个场景积分直接清零?代码里看不出任何原因。直到我翻到一条注释:
# 见需求文档 PRD-201 第三页:连续30天未登录的用户,积分按50%折算 # 该规则来自运营活动“沉睡唤醒”,2022年上线后一直延续那天我差点给写这条注释的人磕一个。这种注释的价值在于它连接了“代码世界”和“业务世界”。代码只是实现,业务规则才是那个需要被理解的源头。没有注释,你面对的就是一串无法解释的数字和判断条件,只能靠猜。
所以我的经验是:当你发现的代码行为明显有“非显而易见性”时——即一个正常编程思维的人绝不会这么写,但这背后是有意为之的业务决策——请务必留下注释,把决策的来源、原因、时间或关联文档编号写清楚。
4.3 公共API的接口契约注释:该写就写
对那些会被其他团队、其他项目甚至外部用户调用的公共 API,我坚定支持写规范的注释。这里不是写给自己看的随笔,而是接口契约——它是给所有对接方看的使用说明书。
用 Doxygen 或 Javadoc 格式去描述参数范围、返回值语义、异常条件、线程安全性,在我看来完全合理。Python 项目里用 docstring 配 Sphinx 生成在线文档,也是同样道理。工具本身没有错,错的是把工具用在不该用的地方。
接口注释模板我一般长这样:
/** * @brief 提交订单并触发支付流程 * @param orderId 订单ID,必须是已创建且未支付状态 * @param paymentMethod 支付渠道枚举,支持 ALIPAY / WECHAT / CARD * @return 支付流水号,可用于后续对账 * @throws InvalidOrderError 订单不存在或状态非法 * @throws PaymentGatewayError 支付渠道调用失败,可重试 */ PaymentReceipt submitOrder(string orderId, PaymentMethod paymentMethod);这种注释是真正有价值的“注释”,因为它描述的是“外部契约”,不是“内部实现”。实现细节你随便换,契约不能变。它帮调用方理解边界条件、避免误用,同时保证了接口演进的稳定性。这跟我前面骂的“i++加1”完全是两码事。
关键区分:给“调用者”看的信息(契约、范围、约束)值得写;给“维护者”看的信息尽量融入代码本身(命名、结构、类型);给“作者自己看”的信息(我暂时这么写、以后改)基本不值得写。
5. 工具链和配套实践:把注释从代码里“请出去”和“拦下来”
5.1 用 Git 提交信息承载“变更理由”,而不是污染源码
很多人写不出代码注释时,喜欢把变更原因写在源码里:
# 2023-05-11 修改:这里加了缓存,因为DB太慢问题在于,源码不是变更日志,一行行历史文本堆在文件里只会让阅读者崩溃。变更理由本来就该写在 Git 提交信息里,那里有作者、时间、完整 diff,还不占源码空间。
我之前团队的做法很简单:每次提交尽量写清“为什么”和“怎么影响”:
fix(order): 修复并发支付导致超卖 原因:扣减库存的 SQL 少了库存条件判断,两个请求同时进入时 后一个请求覆盖前一个的扣减结果。 影响:加库存校验条件,重试时不会重复扣减。这个提交信息在未来git blame任何一个相关行时都会出现。后来的维护者不用去源码里翻注释,就能完整了解这段代码的来龙去脉。这比源码注释高级太多——因为它跟具体改动绑定,不会像注释一样长期留在文件里失真。
5.2 用评审和自动化检查“拦截”无效注释
既然注释是有害的,最好的策略就是别让它进场。我在 code review 环节会重点关注新增注释:看到那种直译型、过时型、凑数型注释,我一律打回。
同时也有一些自动化手段可以用。比如 ESLint 有no-warning-comments可以设置告警级别,处理TODO、FIXME残留问题。有些团队还会用自定义脚本扫描大段的注释行比例,超过阈值就报警。这里给一个简单的思路示例,用脚本统计每个文件的注释行占比:
#!/bin/bash # 统计某个Python文件中的注释行占比(粗略行级判断) total=0 comments=0 while IFS= read -r line; do total=$((total + 1)) trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')" case "$trimmed" in \#*) comments=$((comments + 1)) ;; esac done < "$1" echo "comment ratio: $((comments * 100 / total))%"这类脚本重点不是算出一个绝对精确的数字,而是让团队有个客观抓手:当某个文件的注释占比异常高时,去重构代码而不是继续堆注释。
5.3 注释工具链的几个常见坑:模板、乱码与清理工具
说句题外话,工具层面有几个和注释相关的高频痛点,我顺手一并聊了。
第一个是 IDE 注释模板。很多人用 IDEA 配了一套包含作者、日期、描述的文件头模板,觉得挺规范。但这类模板往往只生成“当初写文件的人”和“当初创建的时间”,文件一旦被多人维护,这些信息就变成过时的历史牌匾。我见过一个文件,文件头写的是 2012 年某前同事的名字,最后修改的人却是 2023 年的新同事。这种信息对阅读者一点忙都帮不上,反而误导。
所以我的建议是:文件头模板可以保留简单的版权声明或包说明,但“作者/日期”这种一定会过期的信息,尽量别放。作者和日期问 Git,别问注释。
第二个是 vscode 注释乱码。很多老项目源码是 GBK 编码,新编辑器默认 UTF-8,打开后注释部分直接成乱码,看得人头皮发麻。解决办法是让编辑器按文件编码自动检测,或者统一转码:
// .vscode/settings.json { "files.encoding": "utf8", "files.autoGuessEncoding": true }第三种场景是“去除源码注释的软件”。如果你接手一个注释污染严重的项目,想批量清理,可以试用开源工具比如stripcomments(Python)或者直接正则扫描。但要非常谨慎,因为字符串字面量里的#、//、/*不该被当注释误删。我建议用成熟工具,而不是自己写正则。更重要的是,删之前先走一遍 Git diff,确认没有误删文档字符串、LICENSE 头、公共类说明等关键信息。
经验提醒:批量删注释前,先
git stash留一条退路。我就干过一次批量删完,发现某个文件里一条注释其实是给外部对接方看的协议说明,删了之后对接方看代码看懵了。后来靠 Git 恢复才救回来。
6. 在真实项目里推进“去注释化”的实践建议
6.1 增量治理,别搞“大扫除”
如果你认同我的判断,决定在团队里推进这件事,切忌一夜之间发动“删注释运动”。代码库里数十条注释,有些是金,有些是屎,你一把火烧了,金也烧没了。
我当时的做法是:定一条新规,新代码一律不写直译型注释,用命名和拆函数让代码自己说话;对旧代码,只在需要修改的模块里做“路过清理”——你改这个函数的时候就顺手把过时注释清掉,把该留的“为什么”补上。三个月下来,整个核心模块的注释质量好了一大截,而且没有因为大规模重构产生额外风险。
同时我会在团队的编码规范里明确写清楚三类注释的处置方式:直译型和废话型直接删;过时型和误导型修正或者删;注释掉的代码一律删,需要时从 Git 找回;只有真正的“为什么”注释和 API 契约注释才允许留下。
6.2 当别人坚持删你注释时怎么办
这个标题发出去之后,我收到最多的反问是:我注释写得好好的,凭什么删?
我的回答是,删注释不是因为“注释没有价值”,而是因为代码库整体才是真正需要维护的产品。注释是代码的旁白,而旁白一多,正剧就容易被人忽略。当年纪越大、项目越久,你会越来越感激那些“一眼看到底”的文件,而不是那种每段逻辑旁边都要读一段散文才能继续的文件。
如果你实在舍不得,可以做一个实验:找自己三个月前写的十个含有大量注释的文件,把注释全部遮住,只看代码,看自己能理解多少。大多数情况下你会惊讶地发现——代码本身已经足够说明了,注释根本可有可无。少数真正需要在注释里解释的“为什么”,你自然会有刻骨铭心的印象。
6.3 常见问题速查表
| 场景 | 应不应该写注释 | 替代方案 |
|---|---|---|
| 变量/函数命名不清 | 不该写 | 改命名,提取函数 |
| 代码逻辑很复杂,自己都绕不清 | 不该写 | 拆函数,简化结构 |
| 记录“为什么这么写”的背景约束 | 应该写 | 保持简短,关联需求编号 |
| 公共API的入参/返回/异常契约 | 应该写 | Doxygen/Javadoc/docstring |
| 记录TODO和待办事项 | 最好别写 | 用任务系统跟踪,代码中引用编号 |
| 注释掉的废弃代码 | 坚决不写 | 删除,靠Git找回 |
| 文件头作者/创建日期 | 别写 | 靠Git blame |
| 说明业务规则来源 | 应该写 | 写需求文档名和关键决策原因 |
个人体会是,敢不敢删注释,其实是一个程序员对“代码质量”要求的分水岭。写注释确实很舒服——它让写的人觉得“我表达清楚了”,让读的人觉得“有人解释过”,大家一起安心。但这种安心是虚幻的。真正能让项目长期活下来的,是代码本身的清晰度、结构的可读性、变更历史的可追踪性,而不是那些写在边上、慢慢变质的旁白。
这个思路后续还能继续往深走:怎么通过领域建模减少注释需求、怎么让 API 文档直接从代码生成、怎么设计团队评审清单来拦截劣质注释。不过那些话题得单独写一篇了,把这次的底线先亮明白:代码不说谎时,注释才有资格开口说话。