乐于分享
好东西不私藏

Rust 注释与文档注释详解:从代码说明到 cargo doc

Rust 注释与文档注释详解:从代码说明到 cargo doc

《Rust 编程实战》系列第 10 篇

在上一篇文章中,我们学习了 Rust 的函数与返回值。

随着代码越来越多,仅仅让程序“能够运行”还不够。我们还需要让其他开发者,甚至未来的自己,能够快速理解代码的作用。

这时就需要使用注释。

Rust 中的注释主要分为两类:

  • • 普通注释:用于说明代码逻辑
  • • 文档注释:用于生成 API 文档

本文将详细介绍:

  • • 单行注释
  • • 多行注释
  • • 文档注释 ///
  • • 模块文档注释 //!
  • • Markdown 格式文档
  • • 文档代码示例
  • • cargo doc 的使用
  • • 文档测试
  • • 注释的最佳实践

什么是注释?

注释是写给开发者看的说明文字。

编译器通常会忽略注释,不会把它们当作程序代码执行。

例如:

fnmain() {
// 输出欢迎信息
println!("Hello, Rust!");
}

程序运行时,只会执行:

println!("Hello, Rust!");

而不会执行注释内容。


为什么需要注释?

注释主要有以下几个作用:

  • • 解释复杂逻辑
  • • 说明特殊处理原因
  • • 记录函数或模块用途
  • • 提醒后续开发者注意事项
  • • 为公共 API 生成文档
  • • 帮助团队协作和项目维护

需要注意:

好的注释应该解释“为什么这样做”,而不是简单重复代码本身。


单行注释

Rust 使用两个斜杠表示单行注释:

//

例如:

fnmain() {
// 定义用户年龄
letage = 18;

// 输出年龄
println!("{}", age);
}

从 // 开始直到这一行结束,都会被视为注释。


行尾注释

注释也可以写在代码后面:

fnmain() {
letport = 8080// Web 服务端口

println!("{}", port);
}

这种方式适合解释简单配置。

不过,如果说明文字较长,更推荐单独写在上一行:

fnmain() {
// Web 服务默认监听端口
letport = 8080;

println!("{}", port);
}

这样可读性更好。


连续多行注释

可以连续使用多个 //

fnmain() {
// 这是一个简单的 Rust 程序
// 用于演示连续多行注释
// 程序会输出一条消息

println!("Rust 注释示例");
}

在实际项目中,这是一种非常常见的写法。


块注释

Rust 也支持块注释:

/*
    注释内容
*/

例如:

fnmain() {
/*
        这里可以写多行注释
        适合临时说明一大段内容
    */


println!("Hello");
}

也可以写成单行:

fnmain() {
/* 用户默认年龄 */
letage = 18;

println!("{}", age);
}

块注释可以嵌套

Rust 的块注释支持嵌套。

例如:

fnmain() {
/*
        外层注释

/*
            内层注释
        */


        继续外层注释
    */


println!("Hello");
}

有些语言不支持块注释嵌套,但 Rust 支持。

这在临时注释一大段代码时比较方便。


临时注释代码

开发时,有时会暂时停用一段代码。

例如:

fnmain() {
println!("程序开始");

/*
    println!("测试代码 1");
    println!("测试代码 2");
    println!("测试代码 3");
    */


println!("程序结束");
}

运行结果:

程序开始
程序结束

不过,长期项目中不建议保留大量被注释掉的旧代码。

因为 Git 已经能够记录历史版本,没有必要把废弃代码一直留在源码中。


什么是文档注释?

普通注释只在源代码中可见。

文档注释除了能在源码中说明功能,还可以通过 Cargo 自动生成 HTML 文档。

Rust 常用的文档注释有两种:

///

和:

//!

它们的作用不同。


使用 /// 为函数编写文档

/// 用于说明紧跟在它后面的代码项。

例如:

/// 计算两个整数的和。
fnadd(a: i32, b: i32->i32 {
    a + b
}

这里的文档注释属于:

fnadd

函数。


为公共函数编写文档

库项目中,公共函数通常会使用 pub

/// 计算两个整数的和。
///
/// # 参数
///
/// - `a`:第一个整数
/// - `b`:第二个整数
///
/// # 返回值
///
/// 返回两个整数相加后的结果。
pubfnadd(a: i32, b: i32->i32 {
    a + b
}

这些内容可以被 cargo doc 转换为网页文档。


文档注释支持 Markdown

Rust 文档注释支持 Markdown 格式。

例如:

/// 创建一个用户。
///
/// # 功能
///
/// 该函数会完成以下操作:
///
/// - 检查用户名
/// - 检查邮箱
/// - 创建用户记录
/// - 返回用户编号
///
/// # 注意
///
/// 用户名不能为空。
pubfncreate_user() {
}

常见 Markdown 语法包括:

  • • 标题
  • • 列表
  • • 加粗
  • • 代码
  • • 链接
  • • 代码块

常用文档章节

Rust 项目中经常使用以下文档章节:

# Examples
# Arguments
# Returns
# Errors
# Panics
# Safety

它们不是强制要求,但已经成为社区常见规范。


Examples 示例

例如:

/// 计算两个整数的和。
///
/// # Examples
///
/// ```
/// let result = 10 + 20;
/// assert_eq!(result, 30);
/// ```
pubfnadd(a: i32, b: i32->i32 {
    a + b
}

生成的文档中会显示可读性很好的代码示例。


Arguments 参数说明

例如:

/// 根据商品价格和数量计算总价。
///
/// # Arguments
///
/// - `price`:商品单价
/// - `quantity`:购买数量
///
/// # Returns
///
/// 返回商品总价。
pubfncalculate_total(price: f64, quantity: u32->f64 {
    price * quantity asf64
}

Errors 错误说明

如果函数返回 Result,建议说明可能出现的错误。

例如:

/// 读取配置文件。
///
/// # Errors
///
/// 当文件不存在、权限不足或内容格式错误时,返回错误。
pubfnread_config() ->Result<String, std::io::Error> {
    std::fs::read_to_string("config.toml")
}

这样调用者可以提前了解错误场景。


Panics 崩溃说明

如果函数在某些情况下会 panic!,建议明确说明。

例如:

/// 获取数组中的第一个元素。
///
/// # Panics
///
/// 当数组为空时会发生 panic。
pubfnfirst_item(items: &[i32]) ->i32 {
    items[0]
}

这对公共 API 很重要。

调用者需要知道函数是否可能直接导致程序终止。


Safety 安全说明

如果函数是 unsafe,必须清楚说明调用要求。

例如:

/// 从原始指针读取一个整数。
///
/// # Safety
///
/// 调用者必须保证:
///
/// - 指针不为空
/// - 指针指向有效的 `i32`
/// - 指针在读取期间保持有效
pubunsafefnread_pointer(ptr: *consti32->i32 {
unsafe { *ptr }
}

unsafe 函数的文档尤其重要。


使用 //! 编写模块文档

//! 用于说明当前模块或当前 Crate。

例如,在 src/lib.rs 顶部:

//! 一个简单的数学工具库。
//!
//! 这个库提供整数加法、减法和乘法等基础功能。

pubfnadd(a: i32, b: i32->i32 {
    a + b
}

这里的文档属于整个库,而不是某一个函数。


/// 与 //! 的区别

语法
作用
///
注释后面的函数、结构体、枚举等项目
//!
注释当前模块或整个 Crate

例如:

//! 用户模块。
//!
//! 提供用户创建、查询和删除功能。

/// 用户数据结构。
pubstructUser {
pub id: u64,
pub name: String,
}

其中:

  • • //! 说明整个用户模块
  • • /// 说明 User 结构体

为结构体编写文档

例如:

/// 用户信息。
///
/// 用于保存系统中的基础用户数据。
pubstructUser {
/// 用户唯一编号。
pub id: u64,

/// 用户名称。
pub name: String,

/// 用户年龄。
pub age: u8,
}

不仅结构体可以写文档,每个字段也可以单独说明。


为枚举编写文档

例如:

/// 订单状态。
pubenumOrderStatus {
/// 订单已经创建,但尚未付款。
    Pending,

/// 订单已经付款。
    Paid,

/// 订单已经发货。
    Shipped,

/// 订单已经取消。
    Cancelled,
}

这样生成的 API 文档会非常清晰。


为常量编写文档

例如:

/// 应用程序默认端口。
pubconst DEFAULT_PORT: u16 = 8080;

为模块编写文档

例如,创建文件:

src/user.rs

内容:

//! 用户管理模块。
//!
//! 该模块负责:
//!
//! - 创建用户
//! - 查询用户
//! - 删除用户

/// 创建一个用户。
pubfncreate_user() {
println!("创建用户");
}

在 lib.rs 中引入:

pubmod user;

使用 cargo doc 生成文档

Cargo 可以自动读取文档注释并生成 HTML 页面。

执行:

cargo doc

生成的文件会放在:

target/doc/

目录中。


自动打开文档

可以执行:

cargo doc --open

Cargo 会完成两件事情:

  1. 1. 生成项目文档
  2. 2. 使用默认浏览器打开文档

这也是平时查看项目 API 文档最方便的方式。


不生成依赖文档

默认情况下,cargo doc 可能会为依赖一起生成文档。

只想生成当前项目文档,可以使用:

cargo doc --no-deps --open

这个命令在实际开发中非常常用。


完整库项目示例

创建一个库项目:

cargo new math_utils --lib

打开:

src/lib.rs

写入:

//! 一个简单的数学工具库。
//!
//! 提供基础的加法和乘法功能。

/// 计算两个整数的和。
///
/// # Arguments
///
/// - `a`:第一个整数
/// - `b`:第二个整数
///
/// # Returns
///
/// 返回两个整数相加后的结果。
///
/// # Examples
///
/// ```
/// use math_utils::add;
///
/// let result = add(10, 20);
///
/// assert_eq!(result, 30);
/// ```
pubfnadd(a: i32, b: i32->i32 {
    a + b
}

/// 计算两个整数的乘积。
///
/// # Examples
///
/// ```
/// use math_utils::multiply;
///
/// assert_eq!(multiply(4, 5), 20);
/// ```
pubfnmultiply(a: i32, b: i32->i32 {
    a * b
}

然后执行:

cargo doc --no-deps --open

浏览器中就能看到自动生成的库文档。


文档中的代码可以测试

Rust 的文档代码块不仅用于展示,还可以作为测试运行。

例如:

/// 计算两个整数的和。
///
/// # Examples
///
/// ```
/// use math_utils::add;
///
/// assert_eq!(add(2, 3), 5);
/// ```
pubfnadd(a: i32, b: i32->i32 {
    a + b
}

执行:

cargo test

Cargo 会检查文档示例是否能够正常编译和运行。

这称为:

Doctest

也就是文档测试。


为什么文档测试很重要?

很多项目的代码不断更新,但文档示例没有同步更新。

最终可能出现:

  • • 文档中的函数名已经不存在
  • • 参数数量发生变化
  • • 返回值类型发生变化
  • • 示例代码无法编译
  • • 使用方法已经过时

文档测试可以帮助开发者及时发现这些问题。

这也是 Rust 文档系统非常强大的地方。


隐藏文档示例中的辅助代码

有时文档示例需要初始化代码,但不希望全部展示给读者。

可以在代码行前加:

#

例如:

/// # Examples
///
/// ```
/// # let enabled = true;
/// if enabled {
///     println!("功能已启用");
/// }
/// ```
pubfnrun() {
}

带 # 的代码仍然会参与测试,但生成文档时通常不会展示。

这适合隐藏:

  • • 初始化代码
  • • 导入代码
  • • 错误处理样板代码
  • • 测试辅助内容

忽略某个文档测试

如果代码仅用于展示,暂时不能运行,可以使用:

/// # Examples
///
/// ```ignore
/// connect_to_remote_server();
/// ```
pubfnconnect() {
}

使用:

ignore

后,Cargo 不会执行这段文档测试。

不过不建议滥用,因为这会失去文档测试的价值。


标记代码不需要运行

还可以使用:

/// ```no_run
/// let server = create_server();
/// server.run();
/// ```
pubfnstart_server() {
}

no_run 表示:

  • • 代码需要通过编译
  • • 但不实际运行

适合:

  • • 启动服务器
  • • 连接数据库
  • • 删除文件
  • • 调用远程接口
  • • 长时间运行任务

普通注释与文档注释对比

类型
语法
主要用途
单行注释
//
解释代码逻辑
块注释
/* */
多行说明或临时注释代码
项目文档注释
///
为函数、结构体、枚举等生成文档
模块文档注释
//!
为模块或 Crate 生成文档

TODO 注释

开发中经常会写:

// TODO: 增加参数校验

表示这里还有功能需要完成。

例如:

fncreate_user(name: &str) {
// TODO: 检查用户名是否为空

println!("创建用户:{}", name);
}

还可以使用:

// FIXME: 当前实现在线程环境下可能有问题

表示这里存在需要修复的问题。

不过,这类注释应该配合任务管理系统使用,避免永久留在代码中。


不推荐的注释方式

重复代码含义

不推荐:

// 把 count 加 1
count += 1;

因为代码本身已经非常清楚。

更有价值的注释应该说明原因:

// 跳过表头,因此从第二行开始统计
count += 1;

注释与代码不一致

例如:

// 默认端口为 8080
letport = 3000;

这比没有注释更加危险。

维护代码时必须同步更新注释。


注释掉大量旧代码

不推荐:

/*
fn old_login() {
    // 几十行旧代码
}
*/

旧代码应该交给 Git 保存。

源码中只保留当前有效实现。


给每一行都写注释

不推荐:

// 定义 a
leta = 10;

// 定义 b
letb = 20;

// 计算结果
letresult = a + b;

// 输出结果
println!("{}", result);

这种注释会让代码显得冗余。

变量名和函数名足够清楚时,不需要逐行解释。


注释最佳实践

优先写清晰的代码

例如:

不推荐:

letx = p * q;

然后写:

// 商品单价乘以数量得到总价

更推荐直接改成:

lettotal_price = unit_price * quantity;

清晰命名往往比注释更有效。


解释为什么,而不是解释是什么

不推荐:

// 等待 3 秒
sleep(Duration::from_secs(3));

更推荐:

// 第三方接口存在短时间限流,重试前等待 3 秒
sleep(Duration::from_secs(3));

公共 API 尽量编写文档注释

例如:

pubfncalculate_total() {
}

如果这是对外使用的函数,应该补充:

  • • 功能
  • • 参数
  • • 返回值
  • • 错误
  • • 示例

这样其他开发者不需要阅读函数内部实现,就能正确使用它。


文档示例尽量可运行

文档中的代码最好能够通过:

cargo test

这样可以避免文档长期失效。


实战:为订单模块编写文档

下面是一个完整示例:

//! 订单金额计算模块。
//!
//! 提供商品小计、折扣金额和最终订单金额计算功能。

/// 计算商品小计。
///
/// # Arguments
///
/// - `price`:商品单价
/// - `quantity`:商品数量
///
/// # Returns
///
/// 返回商品单价与数量相乘后的结果。
///
/// # Examples
///
/// ```
/// let subtotal = 29.9 * 3.0;
/// assert_eq!(subtotal, 89.7);
/// ```
pubfncalculate_subtotal(price: f64, quantity: u32->f64 {
    price * quantity asf64
}

/// 根据折扣率计算折后金额。
///
/// # Arguments
///
/// - `subtotal`:商品小计
/// - `discount_rate`:折扣率,范围为 `0.0..=1.0`
///
/// # Returns
///
/// 当折扣率有效时返回折后金额,否则返回原始小计。
///
/// # Examples
///
/// ```
/// let result = 100.0 * 0.8;
/// assert_eq!(result, 80.0);
/// ```
pubfnapply_discount(subtotal: f64, discount_rate: f64->f64 {
if !(0.0..=1.0).contains(&discount_rate) {
return subtotal;
    }

    subtotal * discount_rate
}

这个模块的文档包含:

  • • 模块用途
  • • 函数用途
  • • 参数说明
  • • 返回值说明
  • • 可运行示例

这就是一个比较规范的 Rust API 文档。


本章小结

注释是提高代码可读性和可维护性的重要工具,而 Rust 的文档注释还可以直接生成专业的 API 文档。

通过本文,我们学习了:

  • • 使用 // 编写单行注释
  • • 使用 /* */ 编写块注释
  • • 块注释嵌套
  • • 使用 /// 为函数和类型编写文档
  • • 使用 //! 为模块和 Crate 编写文档
  • • 文档中的 Markdown 格式
  • • ExamplesErrorsPanics 和 Safety
  • • 使用 cargo doc 生成文档
  • • 使用 cargo doc --no-deps --open 打开项目文档
  • • 文档测试 Doctest
  • • ignore 和 no_run
  • • 注释常见误区和最佳实践

最重要的原则是:

代码负责表达“做什么”,注释负责解释“为什么”。

清晰的代码配合准确的文档,才能让一个 Rust 项目真正具备长期维护价值。


下一篇预告

下一篇我们将学习 Rust 控制流之 if 条件判断,包括:

  • • ifelse if 和 else
  • • 比较运算符
  • • 逻辑运算符
  • • if 表达式
  • • 条件分支返回值
  • • 常见类型错误
  • • 实战:根据用户等级计算折扣