避免文档冗余(Avoiding Redundancy):让 doc comment 只记录名字与签名无法表达的信息 避免文档冗余Avoiding Redundancy让 doc comment 只记录名字与签名无法表达的信息【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rustdoc comment///文档注释是开发者日常打交道最多的文档形式但也是最容易被写成复读机的地方名字已经说了parse_ip_addr_v4注释却还要把函数签名念一遍。本文源自 Google Android 团队 Rust 课程comprehensive-rust中的 meaningful-doc-comments 专题系统讲解冗余文档的表现形式、产生原因与改写方法并结合rustdoc、missing_docslint 与仓库内的真实源码示例给出可直接落地执行的文档写作规则。问题本质名字与签名本身就是文档的一部分课程原文开门见山Names and type signatures communicate a lot of information, dont repeat it in comments!在 Rust 中一个条目的名字、一个函数的签名本身就是该条目/该函数文档的一部分。当你开始写 doc comment 时这部分信息已经被覆盖过了。因此名字承载了条目的职责签名参数类型、返回类型、泛型约束、Result/Option等承载了调用契约的很大一部分字段名承载了字段语义。冗余的注释恰恰是把这三者已经传达的信息又复述了一遍——对 API 使用者来说这是零新增信息的文档只消耗阅读时间不增加理解。这条原则在课程的姊妹章节 Names and Signatures are not full documentation 中得到了对照印证名字和类型是文档的一部分但不等于全部。sync_to_server()这样的名字看不出它会覆盖并丢失并发编辑send(self, email: Email) - Result(), Error看不出返回 Ok 也可能投递失败。两篇文章一正一反共同划出了文档注释的边界名字/签名已覆盖的信息 → 不要重复本篇主题名字/签名未覆盖的行为、契约、陷阱 → 必须写清楚what-isnt-docs 的主题。触发冗余的常见原因课程指出了三类典型来源总是给代码写注释的机械执行这是最自然的入坑方式。把document your code照字面理解却忽略了它的本意intent——补充名字与类型无法表达的信息。工具强制文档覆盖率当 CI 或团队规范要求每个公开项必须有 doc comment时写一行复述名字的注释是最省事的及格方案但这正是低质量文档的温床。对文档的不同模式缺少认知库代码与应用程序代码的文档目标不同详见 Library vs application docs用同一套面面俱到的标准去套所有代码必然产生冗余。四种典型冗余形态与改写示范课程给出了一段可直接对照的示例代码下面按形态逐一拆解。形态一复述名字与类型信息// Repeats name/type information. Can omit! /// Parses an ipv4 from a str. Returns an option for failure modes. fn parse_ip_addr_v4(input: str) - OptionIpAddrV4 { ... }函数名parse_ip_addr_v4已经说明了解析 IPv4 地址签名str - OptionIpAddrV4已经说明了输入是字符串、失败时返回None。注释把这三点又复述了一遍。应删掉或替换为签名没有传达的信息例如输入字符串的格式要求192.0.2.1还是允许前导零失败语义None是解析失败还是空输入是否忽略额外空白、是否大小写敏感。形态二复述字段名显然包含的信息// Repeats information obvious from the field name. Can omit! struct BusinessAsset { /// The customer id. customer_id: u64, }字段名customer_id已经完整表达了客户的 ID。注释The customer id.是纯复述可删。如果该字段存在名字无法表达的约束——比如由财务系统分配、对业务资产唯一——那才是值得写的内容。形态三注释以类型名开头// Mentions the type name first thing, dont do this! /// ServerSynchronizer is an orchestrator that sends local edits [...] struct ServerSynchronizer { ... } // Better! Focuses on purpose. /// Sends local edits [...] struct ServerSynchronizer { ... }rustdoc渲染时注释就挂在ServerSynchronizer的页面上读者已经知道自己在看什么类型。以ServerSynchronizer is ...开头纯粹是重复。改写后直接陈述职责与用途信息密度立刻提升。形态四注释以函数名开头// Mentions the function name first thing, dont do this! /// sync_to_server sends local edits [...] fn sync_to_server(...) // Better! Focuses on function. /// Sends local edits [...] fn sync_to_server(...)同理函数的文档页面由 rustdoc 生成页面上就有函数名。以名字开头的注释只是回声。直接把动词短语作为首句——Sends local edits ...——才是有效的文档开头。判定规则速查冗余形态例子处理方式复述名字/签名/// Parses an ipv4 from a str...删除或补充签名未表达的格式/失败语义复述字段名/// The customer id.删除或补充字段约束以类型名开头/// ServerSynchronizer is an orchestrator...改写为职责陈述以函数名开头/// sync_to_server sends...改写为动词短语开头为什么冗余注释不仅无用还有害课程的 Motivation 部分给出了两个深层的反对理由1. 对 API 使用者零新增信息。文档存在的意义是帮助使用者在名字和签名之外获得新的知识。纯粹复述的注释不提供任何新增内容徒增阅读负担。2. 签名会变注释不会跟着变。这是最容易被忽视的代价。签名的信息参数类型、返回类型会随重构而变化而复述签名的注释往往被遗忘在原地最终形成注释与签名互相矛盾的文档。课程原文signature information may change over time without the documentation being updated accordingly!这一点在 Why and What, not How and Where 中得到呼应解释实现细节SELECT查询、for循环的注释比解释契约的注释过时得更快。冗余注释的过时风险与之同源——它们锚定的是会变的表面信息而非不变的语义。直觉法则写注释前先问还缺什么课程的 Rule of Thumb 值得抄进团队规范What information is missing from a users perspective? Other than name, signature, and irrelevant details of the implementation.翻译过来从使用者的视角看还有哪些信息缺失除了名字、签名和不相关的实现细节之外。逐项检查名字/签名已经说了什么→ 这部分不用写使用者还缺什么→ 这是 doc comment 的职责范围哪些是实现细节且与使用者无关→ 这部分也不要写例如内部用了 for 循环毫无意义。别在注释里普及 Rust 基础知识课程还给出了一个具体的克制原则Dont explain the basics of Rust or the standard library. Assume the reader has an intermediate understanding of the language itself. Focus on documenting your API.如果你的函数返回Result不需要在注释里解释Result是什么、?操作符怎么用——假设读者已经具备中级 Rust 水平注释只负责你的 API 自身。这条约束与 who-are-you-writing-for.md 中的知识诅咒curse of knowledge互为镜像前者防止你低估读者去普及语言基础后者防止你高估读者默认对方懂你的领域黑话。恰当的中间态是不解释Result但为领域术语提供 signpost例如链接到 rustc-dev-guide 或标准库文档。对比标准库与优秀开源代码的少即是多课程的示例章节指出标准库的许多地方文档极少因为名字和类型已经给出了足够的信息。这是冗余为零的正面案例。在 Library vs application docs 中也提到基础库标准库、Serde、Tokio 这类高度可复用的框架往往有详尽文档但详尽的来源是使用场景、并发语义、错误契约等名字无法覆盖的内容而不是对签名的复述。稳定性高、复用面广的代码可以负担详尽文档的 ROI应用程序代码变化频繁详尽文档会迅速过时更需要克制详见该姊妹篇的对照分析。库代码与应用程序代码冗余容忍度的差异课程在details中专门提醒要意识到不同文档模式的不同目的库代码Library code使用者众多、解决的问题跨度大、API 通常稳定。它的文档需要覆盖使用范围scope和用户广度breadth of people因此可以更详尽——但详尽的应是契约、边界条件、错误语义而非复述。应用程序代码Application code使用者少、目标具体、变化频繁。文档可以更简单直接冗余注释在应用代码中的性价比更低——写出来很快就会被改动的代码甩在后面。这意味着冗余的判定没有绝对值而是要结合条目所处的位置公开 API vs 内部函数与受众规模来权衡。相关对照请继续阅读 Library vs application docs。仓库实证课程源码中的克制式文档comprehensive-rust 仓库自身的 Rust 示例代码就是名字 一句话职责风格的活教材。以 src/android/build-rules/library/src/lib.rs 为例//! Greeting library. /// Greet name. pub fn greeting(name: str) - String { format!(Hello {name}, it is very nice to meet you!) }模块级注释//! Greeting library.一句话点明模块职责函数注释/// Greet \name.恰好是函数名 参数名 返回类型**没有**覆盖的信息边界greeting看不出它要做什么str - String看不出语义所以一句话补充问候某人是必要的但注释到此为止没有去解释format!、没有复述返回一个 String正是课程主张的克制。同样地课程 Meaningful Doc Comments 主页用三个反面示例点题/// API for the client // ❌ Lacks detail pub mod client {} /// Function from A to B // ❌ Redundant fn a_to_b(a: A) - B {...} /// Connects to the database. // ❌ Lacks detail fn connect() - Result(), Error {...}/// Function from A to B是冗余的反例a_to_b(a: A) - B这个名字加签名已经把从 A 到 B说完了/// API for the client和/// Connects to the database.则是缺乏细节的反例名字虽然起了提示作用但没说清楚 client API 的能力边界、connect()的失败模式与重试语义。三行代码正好演示了名字/签名与doc comment之间那道需要精确校准的界限——既不能复述也不能空洞。用missing_docs强制覆盖率的正确姿势课程的 More to Explore 部分专门讨论了#![warn(missing_docs)]这个 lint 的副作用The#![warn(missing_docs)]lint can be helpful for enforcing the existence of doc comments, but puts a large burden on developers that could lead to leaning onto these patterns of writing low-quality comments.也就是说强制有注释不等于强制有好注释。在覆盖率压力的驱使下开发者最容易滑向复述名字/签名这类低质量模式——因为那是零思考成本地满足 lint 的方式。这正是冗余注释产生链条中工具驱动那一环的实证。课程给出的启用建议非常克制只有当维护团队有能力跟上其要求时才应启用通常只适用于库风格的 crate而非应用程序代码。从源码结构看这也与 Rust 生态的通行做法一致公开 API 多的库 crate 需要保证每个公开项都有文档因为使用者无法阅读源码而应用代码的读者就是同仓库的同事强制覆盖率带来的收益远小于负担。练习从冗余反向识别真需求课程配套练习 Exercise: Dialog on Details 揭示了冗余注释的另一面不必要的细节有时正是需要文档化的信号。/// Sorts a slice. Implemented using recursive quicksort. fn sort_quicklyT: Ord(to_sort: mut [T]) { ... }表面看用递归快速排序实现是冗余的实现细节对应不要解释 for 循环的忠告但如果这个函数处理不可信数据而快速排序对恶意构造的输入存在已知的二次复杂度攻击课程引用了一篇关于恶意输入导致排序退化的论文那么排序算法在什么输入下会退化就变成了调用方必须知道的契约信息。练习的结论是实现细节是否值得写取决于公开契约例如是否可以喂入不可信数据。这要求作者在复述 for 循环这类无意义细节与隐瞒已知算法风险之间做出审慎判断。换句话说判定一条注释是否冗余最终要回到一个问题它对调用者做决策有没有增量价值总结冗余文档的检查清单结合课程内容把上述规则压缩成一份可日常执行的检查清单签名检查注释是否在复述参数类型、返回类型、Option/Result语义名字检查注释是否以条目自身名字/类型名开头、或复述名字已表达的信息字段检查结构体字段的注释是否只是把字段名翻译成一句话基础检查是否在解释Result、?、for循环等 Rust 基础知识或语言机制实现检查是否在描述内部实现哪个数据库、哪条 SQL、什么循环而调用者并不需要增量检查删掉这条注释读者会损失哪些签名无法覆盖的信息格式约束、失败模式、副作用、性能陷阱、并发语义如果第 1–5 条命中且第 6 条无内容删除或改写如果第 6 条有实质内容哪怕第 1–5 条也部分命中也应改写为聚焦目的与契约的表述——就像课程示范的那样把ServerSynchronizer is an orchestrator that ...改成Sends local edits ...。写出不冗余的 doc comment本质上是把写注释从记录代码现状名字、签名、实现转变为补充代码未表达的知识契约、约束、陷阱、使用场景。掌握这一转变你的 API 文档就能在信息量为零的复读与信息充分的指南之间稳定地落在后者。延伸阅读仓库内相关章节What is documentation (not)? —— 名字与签名覆盖不到的行为正是 doc comment 的职责区Anatomy of a Doc Comment —— 一句话摘要 详细说明 # Examples/# Panics/# Errors/# Safety章节的正确结构Why and What, not How and Where —— 记录契约而非实现细节的扩展讨论Who are you writing for? —— 面向读者写作与知识诅咒Library vs application docs —— 不同代码类型的文档详略权衡Meaningful Doc Comments 章节主页 —— 冗余与空泛两个反例的总览【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考