Rust 注释:从 // 到 rustdoc

11 阅读7分钟

一、先把基础摆正

教程给了四条结论,值得原样记住:

  1. 注释是写给人看的,计算机根本不执行它。
  2. // 开头的是单行注释,可以独占一行,也可以跟在代码后面。
  3. /* ... */ 包起来的是多行注释。
  4. 不需要给每一行都写注释——好代码应该自己说得清楚,注释只留给「不显然」的地方。

对应的三段代码,编译运行后的结果(实测):

fn main() {
    // This is a comment; the computer ignores this line
    let x = 5; // You can also put one after code
    println!("{}", x);
}
// 输出:5
fn main() {
    /*
        This is a multi-line comment
        It can span several lines
        The computer ignores all of it
    */
    let x = 10;
    println!("{}", x);
}
// 输出:10

教程还提到一个高频用法:临时把某行代码「注释掉」,而不是删掉。

fn main() {
    let x = 5;
    // println!("{}", x); // Not printing for now, but don't want to delete it
    println!("Program finished");
}
// 输出:Program finished

这里有个教程没提、但实跑就会撞上的副作用:

warning: unused variable: `x`
 --> c.rs:2:9
  |
2 |     let x = 5;
  |         ^ help: if this is intentional, prefix it with an underscore: `_x`

把唯一使用 x 的那行注释掉之后,x 立刻变成未使用变量,编译器开始告警。注释不是「让代码消失」,它改变了代码本身的语义环境——调试时看到一堆新警告,先想想是不是刚注释掉了什么。


二、编译器视角:注释到底去哪了

「注释不执行」是入门说法,更准确的一句在 The Rust Reference 里:

Non-doc comments are interpreted as a form of whitespace.

也就是说,普通注释在词法分析阶段就被当成空白丢掉了,它连 AST 都进不去,更不会变成任何一条指令。推论很直接:注释写多写少、写英文还是中文,都不影响生成代码和运行性能。

但 Reference 里还有一句转折:

Line doc comments beginning with exactly three slashes (///) ... are interpreted as a special syntax for doc attributes.

文档注释不是空白,它是属性。 这是 Rust 和普通 C 系语言最大的分野:注释在这里升级成了 API 契约的一部分。


三、文档注释:四种写法,两种位置

写法类型等价形式作用对象
///outer 行文档#[doc=" ..."]紧跟其后的那个 item
/** ... */outer 块文档#[doc=" ..."]紧跟其后的那个 item
//!inner 行文档#![doc=" ..."]所在的模块 / crate
/*! ... */inner 块文档#![doc=" ..."]所在的模块 / crate

/// 用在函数、结构体、枚举前面,//! 用在文件开头(描述整个模块或 crate)。因为它们是属性,所以必须出现在能接收属性的东西前面——实测把 /// 孤零零放在文件末尾:

error: expected item after doc comment
 --> f.rs:3:1
  |
3 | /// a doc comment with no item following it
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this doc comment doesn't document anything

不是警告,是编译错误。而普通注释放在哪都无所谓。

「恰好三个」是个坑

Reference 特意强调 exactly three slashes。实测下面这段能正常编译并输出 ok:

fn main() {
    //// four slashes: ordinary comment
    /*** three asterisks: ordinary comment */
    println!("ok");
}

//// 后面没有 item,也完全没事

//// 和 /*** 都只是普通注释。很多人写分隔线时顺手打四个斜杠,本意是「更醒目」,结果它并不会出现在生成的文档里。真要写文档,请严格 ///;真要写分隔线,请坚持 //// 或 // -----,别混。

注释里的代码真的会跑

Rust 最实用的一个设计:/// 里的 Markdown 代码块会被 rustdoc 当成 doctest 执行。我在一个临时 cargo 库里写了两个函数,一个断言正确、一个故意写错:

//! 中文注释示例:Rust 源文件默认 UTF-8,注释里写中文完全没问题。

/// 把两个整数相加。
///
/// # Examples
///
/// ```
/// use docdemo::add;
/// assert_eq!(add(1, 2), 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// 整数除法(取整)。
///
/// 下面这个 doctest 故意写错断言,用来验证注释里的代码真的会被执行。
///
/// ```
/// use docdemo::div;
/// assert_eq!(div(6, 2), 4);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
    a / b
}

cargo test --doc 的结果:

running 2 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 19) ... FAILED

assertion `left == right` failed
  left: 3
 right: 4
test result: FAILED. 1 passed; 1 failed

注释里的示例代码会随 CI 一起被验证,写错就挂。 这比任何「文档与代码同步」的口号都有效。顺便也验证了两件事:中文注释没问题(源文件默认 UTF-8),以及 //! 放在文件头是合法的 crate 级文档。

这里还有个新手常踩的坑:doctest 是独立编译的代码片段,不会自动看见被注释的函数。我最初用单文件 rustdoc --test g.rs 时,注释里直接写 assert_eq!(add(1, 2), 3),报的是:

error[E0425]: cannot find function `add` in this scope

正确写法是显式 use 库名::函数名;(如上例的 use docdemo::add;)。库项目里 cargo test --doc 会把 crate 作为外部依赖注入;单文件模式下则需要自己把代码写全。

块文档注释怕 */

Reference 的 Note 里专门提醒:块文档注释不解析内部 Markdown,遇到第一个 */ 就结束。实测这句:

/** Docs mentioning the `*/` marker inside a block doc comment */

直接编译失败:

error: unknown start of token: `
error: expected one of `!` or `::`, found `inside`

注释在 */ 处提前闭合,后面的文字被当成 Rust 代码解析了。想在文档里写 */ 这种字面量,老实用 /// 行文档,它逐行生效,没有终止符问题。

让编译器逼你写文档

#![warn(missing_docs)]

pub fn public_api() {}

fn main() {}

实测产出两条警告:

warning: missing documentation for the crate
warning: missing documentation for a function

把 #![warn(missing_docs)] 换成 #![deny(missing_docs)],公共 API 漏写文档就直接构建失败。库项目里这一行比代码评审管用。


四、块注释可以嵌套,这点 Rust 和 C/Java 不一样

Reference 写得很干脆:

Comments follow the general C++ style of line (//) and block (/* ... */) comment forms. Nested block comments are supported.

实测:

fn main() {
    /* outer /* inner /* deepest */ still comment */ end */
    let x = 7;
    println!("{}", x);
}
// 输出:7

在 C/C++/Java 里,第一个 */ 就会把整段注释关掉,里面的 /* 只是普通字符,所以「注释掉一大段代码」经常因为代码里原本有块注释而翻车。Rust 支持配对嵌套,可以放心用 /* */ 包住整段代码调试。文档注释的三种块形式之间也能互相嵌套,Reference 的示例里给了完整的组合表。


五、什么时候该写注释

原教程列了三条:逻辑不显然时、怕几天后回头看不懂时、临时停用某行代码时。我补四条工程侧的:

  • 写「为什么」,别写「是什么」。 // 把 i 加 1 是噪音;// 索引从 1 开始,因为上游协议把 0 留作心跳包 才是信息。后者无法靠命名表达。
  • 能用命名和类型解决的,别用注释。 let retry_count = 3; 不需要 // 重试三次;把魔法数字提成常量、把 bool 换成语义明确的枚举,注释自然就消失了。
  • 注释掉的代码不要提交。 临时屏蔽用来调试没问题,但提交前请删掉——版本历史属于 git,不属于注释。而且如第一节所示,它还会顺手制造未使用变量的警告。
  • 给工具链留约定。 // TODO: / // FIXME: / // SAFETY:(unsafe 块说明安全性论证)这类固定前缀,IDE、CI、评审都认;配合 missing_docs 和 cargo doc,让注释进入自动化流程而不是靠自觉。

一句话总结原教程那条提醒:注释的密度和代码质量往往成反比。真正稀缺的不是解释,是被解释的那个决策背后的上下文。


六、速查

场景用什么注意
普通说明//、/* */当空白处理,不影响生成代码
公共 API 文档///恰好三个斜杠;后面必须有 item
模块 / crate 说明//!放文件头,等价 #![doc=...]
文档里放示例/// + ``` 代码块会被 cargo test --doc 执行,记得 use
临时屏蔽代码//别提交;留意连带产生的 warning
大段注释掉代码/* */Rust 支持嵌套,比 C/Java 安全
文档里出现 */改用 ///块文档注释会在第一个 */ 断开

小结

教程那一章讲的是「注释是什么」,一句话就能记住:注释给人看,编译器不管。但真写 Rust 时更有用的是后半句——普通注释是空白,文档注释是属性。前者随便写,后者会被编译、被 rustdoc 收集、被 cargo test --doc 执行,写错斜杠数量、写错断言、写错终止符,都会以编译错误的形式回来找你。

参考