手把手带你写 Rust 文档

0 阅读8分钟

手把手带你写 Rust 文档

在 Rust 中,文档不是代码的“附属品”,而是程序开发的一部分。Rust 为文档提供了原生的工具链支持,达到文档即代码、文档即测试的效果,这也是为什么 Rust 生态的库普遍有极高的文档质量。这篇文章会手把手带你写 Rust 文档,从基础语法、高级技巧,到最佳实践,写出符合 Rust 社区规范、可维护、高可用性的技术文档。

rustdoc 入门

Rust 文档的核心工具是 rustdoc,它是 Rust 官方内置的文档生成器,和 Cargo 深度集成,可以通过下面的命令使用:

rustdoc --version
# cargo doc 是调度 rustdoc 的便捷封装
cargo doc --version

命令速查

在项目根目录执行以下命令,就能完成文档的生成、预览和测试:

命令作用
cargo doc生成当前项目及所有依赖的文档
cargo doc --open生成文档并自动在浏览器打开预览
cargo doc --no-deps仅生成当前项目的文档,跳过依赖,大幅提升生成速度
cargo test --doc文档测试,会运行文档中的示例代码
cargo doc --offline离线生成文档,无需网络连接

文档注释语法

Rust 严格区分了普通注释和文档注释:普通注释是给代码维护者看的,不会被 rustdoc 解析,而文档注释是给用户看的,会被渲染到文档页面中。

四种注释的区别与用法

注释语法类型作用范围用途
// 内容普通行注释仅代码内部给维护者写的代码说明、TODO 等
/* 内容 */普通块注释仅代码内部多行的代码内部说明
/// 内容行文档注释紧随其后的项(函数、结构体、枚举等)给公开的 API 项写文档
/** 内容 */块文档注释紧随其后的项多行的 API 项文档,用法和 /// 一致
//! 内容模块级行文档注释所在的模块写在文件顶部,给当前模块写文档

第一行文档注释

我们先给一个简单的加法函数写文档,如下所示:

/// 计算两个整数的和,返回相加后的结果
///
/// 这是一个基础的整数加法函数,支持正负数、零的运算,
/// 不会做溢出检查,溢出行为遵循 Rust 整数溢出规则
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

执行 cargo doc --open,你就能在文档中看到这个函数的完整说明,rustdoc 会自动把注释内容渲染到函数的详情页中。

add函数注释文档

Markdown 的支持

rustdoc 对 Markdown 的支持还是比较全面的,包括代码块(带语法高亮)、列表、链接、表格等标准的 Markdown 语法。

基础 Markdown 语法

你可以在文档注释中使用所有常用的 Markdown 语法:

  • 标题:# 一级标题## 二级标题,推荐二级及以下标题用于文档内部分段
  • 列表:有序列表 1. xxx、无序列表 - xxx
  • 代码块:用于写可运行的示例
  • 强调:**加粗***斜体*
  • 链接:[链接文本](链接地址)
  • 表格:常用于参数对比、特性说明等
  • 引用块:> 内容,常用于提示、警告信息

文档内链接(Intra-doc Links)

这是 Rust 文档的特性之一:你可以在文档中直接链接到当前 crate、标准库、其他 crate 的任意 API 项,用户点击就能跳转到对应页面。

语法比较简单,直接用[项名]即可完成链接,rustdoc 会自动解析路径并生成跳转链接:

/// 数学运算相关的错误类型枚举
#[derive(Debug, PartialEq, Eq)]
pub enum MathError {
    /// 除零错误,当除数为 0 时触发
    DivideByZero,
    /// 整数溢出错误
    Overflow,
}

/// 整数除法运算,返回商或错误
///
/// # Errors
/// 当除数 `b` 为 0 时,返回 [MathError::DivideByZero]
pub fn divide(a: i32, b: i32) -> Result<i32, MathError> {
    if b == 0 {
        return Err(MathError::DivideByZero);
    }
    Ok(a / b)
}

上面的例子中,[MathError::DivideByZero] 会自动生成跳转到该枚举项的链接,用户点击就能看到对应的定义和说明。

文档即测试

Rust 文档中最惊艳的功能之一就是文档测试(Doctest),这是其他编程语言中基本没有的功能。rustdoc 会把你文档里的 rust 代码块,当成单元测试来编译运行。这也就解决了文档和代码不同步的痛点,只要你改了代码,文档里的示例跑不通,测试就会直接报错,强制你同步更新文档。

第一个文档测试

我们给 add 函数的文档加上可运行的测试:

/// 计算两个整数的和,返回相加后的结果
///
/// # 示例
/// ```rust
/// use doc_example::add;
///
/// // 基础加法
/// assert_eq!(add(2, 3), 5);
/// // 正负数相加
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

执行 cargo test --doc,你会看到如下输出:

running 1 test
test src/lib.rs - add (line 22) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

隐藏辅助代码

很多时候,示例需要一些导入、main 函数等辅助代码,但这些代码会让文档变得冗余。你可以用 # 开头的行,标记这段代码会被编译运行,但不会渲染在文档中:

/// 除法运算示例
///
/// ```rust
/// # use doc_example::{divide, MathError};
/// # fn main() -> Result<(), MathError> {
/// let result = divide(10, 2)?;
/// assert_eq!(result, 5);
/// # Ok(())
/// # }
/// ```

渲染后的文档只会显示核心的两行代码,隐藏的 usemain 函数会在测试时正常运行,完美解决了 Result 类型的错误处理问题。

测试属性标记

你可以在代码块的开头添加属性,控制测试的行为:

属性作用
rust,should_panic标记该示例预期会 panic,测试时 panic 才算通过
rust,ignore忽略该示例,不编译不运行(常用于展示性代码)
rust,no_run只编译不运行(常用于网络 IO、文件操作等无法自动化测试的场景)
rust,compile_fail预期该代码编译失败,测试时编译不通过才算通过

示例如下:

/// 除零场景示例
///
/// ```rust,should_panic
/// use doc_example::divide;
///
/// // 除零会触发 panic
/// divide(10, 0).unwrap();
/// ```

非 Rust 代码块

如果你需要展示其他格式的内容,比如 JSON、文本、Shell 命令,只需要指定对应的语言标识,rustdoc 就不会把它当成测试:

/// 配置文件格式示例
///
/// ```toml
/// [math]
/// precision = 10
/// enable_overflow_check = true
/// ```

渲染 Mermaid 流程图

rustdoc 并不支持原生渲染 Mermaid 流程图,但是我们可以使用 aquamarine 这个库来达成效果,它是 rustdoc 的过程式宏扩展。

步骤一:添加依赖
cargo add aquamarine
步骤二:使用 Mermaid 流程图
#[cfg_attr(doc, aquamarine::aquamarine)]
/// 计算器运算流程
///
/// ```mermaid
/// graph LR
/// A[设置初始值] --> B[加法运算]
/// B --> C{是否除零?}
/// C -->|否| D[除法运算]
/// C -->|是| E[返回除零错误]
/// D --> F[输出最终结果]
/// ```
pub fn example() {}

渲染后的效果如下:

计算器运算流程

文档属性

Rust 提供了一系列 #[doc] 属性,让开发者可以精细化控制文档的渲染、可见性、搜索等行为,满足复杂场景的需求。

属性作用示例
#[doc = "内容"]动态生成文档内容,常用于宏生成的代码#[doc = concat!("这是", "动态生成", "的文档")]
#[doc(hidden)]隐藏该项,不渲染在文档中(用于内部实现)
#[doc(alias = "别名")]#[doc(alias = "别名")] 给项添加搜索别名,用户搜索别名也能找到该项
#[doc(inline)]内联重导出项的文档,用户无需跳转就能看到
#[doc(no_inline)]不内联文档,点击跳转到原文档页面
#[cfg(doc)]仅在生成文档时编译该段代码
#![deny(missing_docs)]强制所有公开项必须有文档,缺失则编译报错写在 lib.rs 顶部,用于团队规范约束

最佳实践

Rust 社区有一套公认的文档规范,如下所示:

  • 一句话摘要:文档的第一行必须是一句话,精准概括这个项的核心作用
  • 详细描述:补充适用场景、设计思路、边界情况,告诉用户“什么时候用、什么时候不该用”
  • 示例优先:先给可运行的核心示例,再讲参数、返回值等细节
  • 专项说明:按需添加 # Parameters# Returns# Errors# Panics# Safety# Performance 等专项章节
  • 公开项必写文档:所有 pub 的项都必须有文档,用 #![deny(missing_docs)] 强制约束
  • 文档和代码同步:改了代码不改文档,文档测试就是为了解决这个问题,确保每个示例都能跑通
  • unsafe 函数必须写 Safety 说明:这是 Rust 社区的硬性规范,所有 unsafe 函数必须写清楚调用者需要遵守的安全约束

最后,想要写出高质量的文档,最好的方法就是学习顶级开源库的写法,比如 Rust 标准库文档,它应该是最权威的文档规范范本了,另外,像 serde、tokio、anyhow 等库的文档也值得拿来学习。

总结

Rust 文档的核心,从来都不是语法和工具,而是“站在用户的角度思考”,你写的每一行文档,都是为了让用户能快速理解、正确使用你的代码。