1984年,Donald Knuth在斯坦福大学提出了一个激进的设想:程序员应该像散文家一样写作。在他著名的论文《Literate Programming》中,Knuth写道:“让我们改变传统的编程观念:不再想象我们的主要任务是指示计算机做什么,而是专注于向人类解释我们想让计算机做什么。”
四十多年后的今天,打开任何一个现代开源项目,你会发现一个令人困惑的现象:注释似乎正在消失。大型开源项目中注释密度在过去十年间显著下降,而一些语言(如Go)甚至在其官方风格指南中暗示"好的代码不需要注释"。
从Knuth的文学化编程梦想到今天的"自文档化代码"理念,代码注释经历了怎样的演变?为什么现代代码看起来需要的注释越来越少?这是进步还是倒退?
文学化编程:一个美丽的失败
Knuth的文学化编程并非简单的"多写注释"。这是一种全新的编程范式:代码和文档不再是分离的两个实体,而是融合在一起的作品。程序员使用WEB工具,将代码组织成一个个"片断"(snippet),每个片断都有详细的散文式说明,最终通过"编织"(weave)生成文档,通过"缠结"(tangle)生成可执行代码。
这个理念在当时颇具影响力。Knuth用这套方法编写了著名的TeX排版系统,代码清晰得像教科书。然而,文学化编程最终未能成为主流。
问题在于成本。文学化编程要求程序员投入大量精力在文档撰写上,而软件行业的快速迭代节奏让这种理想化的实践变得不切实际。更重要的是,当代码发生变化时,文档也需要同步更新——这是文学化编程无法回避的维护负担。
PostgreSQL项目的演化研究揭示了一个有趣的现象:在二十年的开发历史中,被注释函数的百分比几乎保持恒定。研究者在2006年的分析中发现,除了早期因注释风格变化导致的波动外,注释比例长期稳定在一个相对固定的水平。这暗示了注释与代码之间存在某种微妙的平衡关系,而非简单的"越多越好"或"越少越好"。
注释的阴暗面:学术研究揭示了什么
2019年,一项大规模实证研究分析了超过13亿次AST级别的代码变更,试图理解代码和注释如何协同演化。研究结果令人震惊:代码与注释的不一致变更比一致性变更高出约1.5倍的bug引入风险。
这个问题比想象中更普遍。2024年发表的研究《Are your comments outdated?》提出了CoCC方法,用于自动检测代码-注释一致性。研究者识别了15个导致注释过时的关键因素,并发现这一问题在不同编程语言中都存在。Precision超过90%的检测结果表明,注释过时是一个系统性的、可预测的问题。
这不是个例。软件工程领域对代码注释质量进行了长达十年的系统性文献综述。2022年发表的研究分析了47篇相关论文,识别出21个用于评估注释质量的属性。其中最受关注的是一致性(consistency)、完整性(completeness)和可读性(readability)。研究表明,不一致的注释不仅无助于代码理解,反而会误导开发者。
眼动追踪研究提供了更直观的证据。研究者通过追踪开发者阅读代码时的眼球运动发现,注释确实能降低复杂代码的认知负荷——但前提是注释质量足够高。低质量的注释不仅没有帮助,反而分散了开发者的注意力。
这些研究揭示了一个残酷的事实:注释是有成本的。它们需要维护,需要与代码保持同步,否则就会成为技术债务。这解释了为什么现代编程实践开始重新审视注释的价值。
类型系统:当代码开始"说话"
2010年,TypeScript的创造者Anders Hejlsberg面临一个设计选择:如何在JavaScript生态中引入类型系统?他的答案是让类型签名成为文档的一部分。
这不是空想。Haskell社区早在上世纪90年代就证明了类型签名的文档价值。一个精心设计的类型签名parse :: String -> Either ParseError AST比任何注释都能更精确地描述函数的行为:它接受一个字符串,返回解析成功后的抽象语法树或解析错误。
TypeScript继承了这一理念。Effective TypeScript一书甚至专门用一节来讨论"不要在文档中重复类型信息"。作者指出,TypeScript的类型注解系统本身就设计得紧凑、描述性强且可读。当函数签名明确标注function formatDate(date: Date, format: string): string时,额外的注释// formats the date不仅多余,还增加了维护负担。
Rust将这一理念推向了新高度。Rust的类型系统不仅表达输入输出类型,还通过Option、Result等类型编码了错误处理的语义。更重要的是,Rust的rustdoc工具将文档注释转化为可测试的文档:每个代码示例都会被编译和执行,确保文档永远不会过时。这种设计强制性地解决了注释维护问题。
2023年的ECMAScript提案"Types as Comments"试图将这一理念带回JavaScript。该提案允许类型注解作为注释存在,在运行时被完全忽略,但在开发时提供完整的类型信息。这正是"类型即文档"理念的终极体现。
不同语言的注释文化
不同编程语言发展出了截然不同的注释文化,这反映了语言设计哲学的差异。
Go语言的极简主义
Go的官方代码评审评论指南中有一条耐人寻味的建议:“注释应该解释做了什么、为什么做、有什么影响,而不是怎么做的。“这反映了Go的设计哲学:代码本身应该足够清晰来表达"怎么做”。
Go标准库中的代码常常被用作"自文档化代码"的典范。函数名和变量名经过精心选择,避免了需要注释的命名如tmp、data、value。当一个函数需要注释才能理解时,Go程序员倾向于重命名或重构,而非添加注释。
Java的Javadoc传统
Java选择了完全不同的道路。从1995年开始,Javadoc就成为了Java生态的核心组成部分。Oracle官方指南详细规定了如何编写文档注释,包括标签的使用规范、句子结构甚至标点符号。
这种传统源于Java的时代背景。上世纪90年代的IDE功能有限,开发者需要外部文档来理解库的行为。Javadoc通过从代码中提取注释生成HTML文档,创造了一种可持续维护的文档模式。
但Javadoc也有批评者。批评者指出,大量Javadoc注释只是简单地重复方法签名,如/** Returns the user's name. */ public String getName()。这种注释不仅没有增加信息,反而增加了代码阅读的认知负担。
Python的文档字符串哲学
Python在PEP 257中区分了文档字符串(docstring)和注释。文档字符串是代码的一部分,描述"如何使用"代码;而注释描述"如何修改"代码。这种区分反映了Python对开发者体验的重视。
Python 3.5引入的类型提示(Type Hints)进一步减少了对某些注释的需求。一个标注了def greet(name: str) -> str的函数,其类型信息已经清晰到不需要额外的参数说明。
认知科学视角:注释如何影响代码理解
代码阅读不仅仅是符号解析,更是一个认知过程。认知心理学研究表明,人类的工作记忆容量有限,大约只能同时处理4-7个信息块(chunk)。
注释的作用取决于它是否减少了认知负荷。好的注释通过提供上下文、解释业务逻辑或说明设计决策,帮助开发者快速建立心智模型。差的注释则增加了信息处理负担,却未提供有价值的信息。
软件工程师常说"代码是写给人看的,只是顺便给机器执行”。这句话源自1985年出版的经典教材《Structure and Interpretation of Computer Programs》。但有一个微妙的问题:代码是写给"什么样的人"看的?
对于熟悉项目上下文的资深开发者,简洁的代码可能足够清晰。但对于新加入的团队成员,同样的代码可能完全无法理解。注释的价值很大程度上取决于读者的背景知识。
这正是注释的两难困境:注释的受众是不明确的。为初学者写的注释对资深开发者是噪音;为资深开发者省略的注释对初学者是障碍。没有放之四海而皆准的注释策略。
AI时代:自动生成文档的利与弊
GitHub Copilot和其他AI编码助手的兴起正在改变代码文档化的格局。这些工具可以自动生成函数注释、解释代码片段甚至生成README文档。
2025年,GitHub官方博客专门撰文讨论如何使用Copilot来文档化和解释遗留代码。AI工具展示了生成高质量注释的潜力,但也带来了新的问题:AI生成的注释是否准确?开发者是否会过度依赖AI而忽视代码本身的清晰性?
更深层的问题是:如果AI可以随时生成注释,我们是否还需要在代码中维护注释?一些开发者开始主张,真正需要的是可执行的文档——如测试用例——而非静态的注释。测试用例永远不会过时,因为它们要么通过,要么失败。
何时该写注释:实践指南
“代码告诉你怎么做,注释告诉你为什么”——Jeff Atwood的这个总结简洁地捕捉了注释的本质。但实践中如何判断?
注释应该出现在以下场景:
-
解释业务逻辑背后的原因。当代码实现了一个非显而易见的业务规则,注释应该解释这个规则的来源。例如,一个计算退休金的函数可能包含了来自税法的特殊条款,这正是需要注释记录的内容。
-
说明性能优化的权衡。如果为了性能牺牲了代码的清晰性,注释应该解释这个决策。Knuth自己说过:“过早优化是万恶之源”,但有时优化是必要的——这时需要注释记录优化动机和测量结果。
-
记录复杂的算法或数学推导。不是所有算法都能通过命名来解释。当涉及非平凡的数学计算时,注释可以提供公式推导或参考文献。
-
标记技术债务和待办事项。TODO、FIXME等标记是特殊的注释,它们不是解释代码,而是标记未来需要改进的地方。这类注释有明确的语义,便于工具提取和跟踪。
注释应该避免出现在以下场景:
-
重复代码逻辑。
i++ // increment i这类注释比无用更糟——它们增加了视觉噪音,却没有提供任何信息。 -
解释语法或API。当代码
list.sort(reverse=True)已经足够清晰时,注释# sort in descending order就是多余的。 -
替代清晰的命名。如果
int d; // elapsed time in days是必要的,那么应该直接写int elapsedTimeInDays;。Martin Fowler在《Refactoring》中明确指出,好的命名可以消除大量注释需求。 -
注释掉不再使用的代码。版本控制系统已经记录了所有历史,注释掉的代码只会造成困惑。删除它,需要时从历史中恢复。
注释的未来
代码注释正在经历一场根本性的转变。这不是注释的消亡,而是注释角色的重新定位。
从文学化编程的宏大叙事,到自文档化代码的实用主义,我们学到了一个重要教训:文档不是代码的附属品,而是代码的一部分。当类型系统、命名规范、测试用例都能承担文档功能时,传统注释的价值确实在下降。
但这并不意味着注释变得不重要。相反,注释的价值变得更加清晰:它不再是为了解释代码怎么做,而是记录代码为什么这样做。当一个函数calculatePension()需要一个注释来解释税法条款时,这个注释的价值是任何类型系统都无法替代的。
五十年的编程实践告诉我们一个简单的真理:好的代码需要好的文档,但好的文档不一定是注释。类型签名、函数名、测试用例、提交信息——这些都是文档的一部分。注释只是这个文档生态系统中的一种工具,而非唯一工具。
Knuth的理想也许没有完全实现,但他的洞察依然有价值:程序是为人类阅读而写的。当代码本身就能讲述自己的故事时,我们不需要额外的旁白。但当故事背后有更深层的原因时,注释就是最好的旁白。
注释没有消亡,它只是找到了自己应该在的位置。
参考文献
-
Knuth, D. E. (1984). Literate Programming. The Computer Journal, 27(2), 97-111.
-
Fowler, M. (2005). Code As Documentation. https://martinfowler.com/bliki/CodeAsDocumentation.html
-
Atwood, J. (2006). Code Tells You How, Comments Tell You Why. Coding Horror. https://blog.codinghorror.com/code-tells-you-how-comments-tell-you-why/
-
Jiang, Z. M., & Hassan, A. E. (2006). Examining the Evolution of Code Comments in PostgreSQL. Proceedings of the 2006 International Workshop on Mining Software Repositories.
-
Wen, Z., et al. (2019). A Large-Scale Empirical Study on Code-Comment Inconsistencies. IEEE International Conference on Program Comprehension.
-
Rani, P., et al. (2022). A Decade of Code Comment Quality Assessment: A Systematic Literature Review. Journal of Systems and Software.
-
Li, Z., et al. (2024). Are your comments outdated? Towards automatically detecting code-comment consistency. Journal of Software: Evolution and Process.
-
Abelson, H., & Sussman, G. J. (1985). Structure and Interpretation of Computer Programs. MIT Press.
-
Ousterhout, J. (2018). A Philosophy of Software Design. Yaknyam Press.
-
Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall.