《Rust 编程实战》系列第 10 篇
在上一篇文章中,我们学习了 Rust 的函数与返回值。
随着代码越来越多,仅仅让程序"能够运行"还不够。我们还需要让其他开发者,甚至未来的自己,能够快速理解代码的作用。
这时就需要使用注释。
Rust 中的注释主要分为两类:
-
普通注释:用于说明代码逻辑
-
文档注释:用于生成 API 文档
本文将详细介绍:
-
单行注释
-
多行注释
-
文档注释
/// -
模块文档注释
//! -
Markdown 格式文档
-
文档代码示例
-
cargo doc的使用 -
文档测试
-
注释的最佳实践
什么是注释?
注释是写给开发者看的说明文字。
编译器通常会忽略注释,不会把它们当作程序代码执行。
例如:
arduino
fn main() {
// 输出欢迎信息
println!("Hello, Rust!");
}
程序运行时,只会执行:
arduino
println!("Hello, Rust!");
而不会执行注释内容。
为什么需要注释?
注释主要有以下几个作用:
-
解释复杂逻辑
-
说明特殊处理原因
-
记录函数或模块用途
-
提醒后续开发者注意事项
-
为公共 API 生成文档
-
帮助团队协作和项目维护
需要注意:
好的注释应该解释"为什么这样做",而不是简单重复代码本身。
单行注释
Rust 使用两个斜杠表示单行注释:
arduino
//
例如:
csharp
fn main() {
// 定义用户年龄
let age = 18;
// 输出年龄
println!("{}", age);
}
从 // 开始直到这一行结束,都会被视为注释。
行尾注释
注释也可以写在代码后面:
csharp
fn main() {
let port = 8080; // Web 服务端口
println!("{}", port);
}
这种方式适合解释简单配置。
不过,如果说明文字较长,更推荐单独写在上一行:
csharp
fn main() {
// Web 服务默认监听端口
let port = 8080;
println!("{}", port);
}
这样可读性更好。
连续多行注释
可以连续使用多个 //:
arduino
fn main() {
// 这是一个简单的 Rust 程序
// 用于演示连续多行注释
// 程序会输出一条消息
println!("Rust 注释示例");
}
在实际项目中,这是一种非常常见的写法。
块注释
Rust 也支持块注释:
arduino
/*
注释内容
*/
例如:
arduino
fn main() {
/*
这里可以写多行注释
适合临时说明一大段内容
*/
println!("Hello");
}
也可以写成单行:
csharp
fn main() {
/* 用户默认年龄 */
let age = 18;
println!("{}", age);
}
块注释可以嵌套
Rust 的块注释支持嵌套。
例如:
rust
fn main() {
/*
外层注释
/*
内层注释
*/
继续外层注释
*/
println!("Hello");
}
有些语言不支持块注释嵌套,但 Rust 支持。
这在临时注释一大段代码时比较方便。
临时注释代码
开发时,有时会暂时停用一段代码。
例如:
arduino
fn main() {
println!("程序开始");
/*
println!("测试代码 1");
println!("测试代码 2");
println!("测试代码 3");
*/
println!("程序结束");
}
运行结果:
程序开始
程序结束
不过,长期项目中不建议保留大量被注释掉的旧代码。
因为 Git 已经能够记录历史版本,没有必要把废弃代码一直留在源码中。
什么是文档注释?
普通注释只在源代码中可见。
文档注释除了能在源码中说明功能,还可以通过 Cargo 自动生成 HTML 文档。
Rust 常用的文档注释有两种:
arduino
///
和:
arduino
//!
它们的作用不同。
使用 /// 为函数编写文档
/// 用于说明紧跟在它后面的代码项。
例如:
rust
/// 计算两个整数的和。
fn add(a: i32, b: i32) -> i32 {
a + b
}
这里的文档注释属于:
csharp
fn add
函数。
为公共函数编写文档
库项目中,公共函数通常会使用 pub:
rust
/// 计算两个整数的和。
///
/// # 参数
///
/// - `a`:第一个整数
/// - `b`:第二个整数
///
/// # 返回值
///
/// 返回两个整数相加后的结果。
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
这些内容可以被 cargo doc 转换为网页文档。
文档注释支持 Markdown
Rust 文档注释支持 Markdown 格式。
例如:
csharp
/// 创建一个用户。
///
/// # 功能
///
/// 该函数会完成以下操作:
///
/// - 检查用户名
/// - 检查邮箱
/// - 创建用户记录
/// - 返回用户编号
///
/// # 注意
///
/// 用户名不能为空。
pub fn create_user() {
}
常见 Markdown 语法包括:
-
标题
-
列表
-
加粗
-
代码
-
链接
-
代码块
常用文档章节
Rust 项目中经常使用以下文档章节:
bash
# Examples
# Arguments
# Returns
# Errors
# Panics
# Safety
它们不是强制要求,但已经成为社区常见规范。
Examples 示例
例如:
rust
/// 计算两个整数的和。
///
/// # Examples
///
/// ```
/// let result = 10 + 20;
/// assert_eq!(result, 30);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
生成的文档中会显示可读性很好的代码示例。
Arguments 参数说明
例如:
rust
/// 根据商品价格和数量计算总价。
///
/// # Arguments
///
/// - `price`:商品单价
/// - `quantity`:购买数量
///
/// # Returns
///
/// 返回商品总价。
pub fn calculate_total(price: f64, quantity: u32) -> f64 {
price * quantity as f64
}
Errors 错误说明
如果函数返回 Result,建议说明可能出现的错误。
例如:
rust
/// 读取配置文件。
///
/// # Errors
///
/// 当文件不存在、权限不足或内容格式错误时,返回错误。
pub fn read_config() -> Result<String, std::io::Error> {
std::fs::read_to_string("config.toml")
}
这样调用者可以提前了解错误场景。
Panics 崩溃说明
如果函数在某些情况下会 panic!,建议明确说明。
例如:
rust
/// 获取数组中的第一个元素。
///
/// # Panics
///
/// 当数组为空时会发生 panic。
pub fn first_item(items: &[i32]) -> i32 {
items[0]
}
这对公共 API 很重要。
调用者需要知道函数是否可能直接导致程序终止。
Safety 安全说明
如果函数是 unsafe,必须清楚说明调用要求。
例如:
rust
/// 从原始指针读取一个整数。
///
/// # Safety
///
/// 调用者必须保证:
///
/// - 指针不为空
/// - 指针指向有效的 `i32`
/// - 指针在读取期间保持有效
pub unsafe fn read_pointer(ptr: *const i32) -> i32 {
unsafe { *ptr }
}
unsafe 函数的文档尤其重要。
使用 //! 编写模块文档
//! 用于说明当前模块或当前 Crate。
例如,在 src/lib.rs 顶部:
rust
//! 一个简单的数学工具库。
//!
//! 这个库提供整数加法、减法和乘法等基础功能。
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
这里的文档属于整个库,而不是某一个函数。
/// 与 //! 的区别
| 语法 | 作用 |
|---|---|
/// |
注释后面的函数、结构体、枚举等项目 |
//! |
注释当前模块或整个 Crate |
例如:
rust
//! 用户模块。
//!
//! 提供用户创建、查询和删除功能。
/// 用户数据结构。
pub struct User {
pub id: u64,
pub name: String,
}
其中:
-
//!说明整个用户模块 -
///说明User结构体
为结构体编写文档
例如:
rust
/// 用户信息。
///
/// 用于保存系统中的基础用户数据。
pub struct User {
/// 用户唯一编号。
pub id: u64,
/// 用户名称。
pub name: String,
/// 用户年龄。
pub age: u8,
}
不仅结构体可以写文档,每个字段也可以单独说明。
为枚举编写文档
例如:
ruby
/// 订单状态。
pub enum OrderStatus {
/// 订单已经创建,但尚未付款。
Pending,
/// 订单已经付款。
Paid,
/// 订单已经发货。
Shipped,
/// 订单已经取消。
Cancelled,
}
这样生成的 API 文档会非常清晰。
为常量编写文档
例如:
rust
/// 应用程序默认端口。
pub const DEFAULT_PORT: u16 = 8080;
为模块编写文档
例如,创建文件:
bash
src/user.rs
内容:
rust
//! 用户管理模块。
//!
//! 该模块负责:
//!
//! - 创建用户
//! - 查询用户
//! - 删除用户
/// 创建一个用户。
pub fn create_user() {
println!("创建用户");
}
在 lib.rs 中引入:
rust
pub mod user;
使用 cargo doc 生成文档
Cargo 可以自动读取文档注释并生成 HTML 页面。
执行:
cargo doc
生成的文件会放在:
bash
target/doc/
目录中。
自动打开文档
可以执行:
arduino
cargo doc --open
Cargo 会完成两件事情:
- 生成项目文档
- 使用默认浏览器打开文档
这也是平时查看项目 API 文档最方便的方式。
不生成依赖文档
默认情况下,cargo doc 可能会为依赖一起生成文档。
只想生成当前项目文档,可以使用:
css
cargo doc --no-deps --open
这个命令在实际开发中非常常用。
完整库项目示例
创建一个库项目:
sql
cargo new math_utils --lib
打开:
bash
src/lib.rs
写入:
rust
//! 一个简单的数学工具库。
//!
//! 提供基础的加法和乘法功能。
/// 计算两个整数的和。
///
/// # Arguments
///
/// - `a`:第一个整数
/// - `b`:第二个整数
///
/// # Returns
///
/// 返回两个整数相加后的结果。
///
/// # Examples
///
/// ```
/// use math_utils::add;
///
/// let result = add(10, 20);
///
/// assert_eq!(result, 30);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// 计算两个整数的乘积。
///
/// # Examples
///
/// ```
/// use math_utils::multiply;
///
/// assert_eq!(multiply(4, 5), 20);
/// ```
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
然后执行:
css
cargo doc --no-deps --open
浏览器中就能看到自动生成的库文档。
文档中的代码可以测试
Rust 的文档代码块不仅用于展示,还可以作为测试运行。
例如:
rust
/// 计算两个整数的和。
///
/// # Examples
///
/// ```
/// use math_utils::add;
///
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
执行:
bash
cargo test
Cargo 会检查文档示例是否能够正常编译和运行。
这称为:
Doctest
也就是文档测试。
为什么文档测试很重要?
很多项目的代码不断更新,但文档示例没有同步更新。
最终可能出现:
-
文档中的函数名已经不存在
-
参数数量发生变化
-
返回值类型发生变化
-
示例代码无法编译
-
使用方法已经过时
文档测试可以帮助开发者及时发现这些问题。
这也是 Rust 文档系统非常强大的地方。
隐藏文档示例中的辅助代码
有时文档示例需要初始化代码,但不希望全部展示给读者。
可以在代码行前加:
bash
#
例如:
perl
/// # Examples
///
/// ```
/// # let enabled = true;
/// if enabled {
/// println!("功能已启用");
/// }
/// ```
pub fn run() {
}
带 # 的代码仍然会参与测试,但生成文档时通常不会展示。
这适合隐藏:
-
初始化代码
-
导入代码
-
错误处理样板代码
-
测试辅助内容
忽略某个文档测试
如果代码仅用于展示,暂时不能运行,可以使用:
csharp
/// # Examples
///
/// ```ignore
/// connect_to_remote_server();
/// ```
pub fn connect() {
}
使用:
ignore
后,Cargo 不会执行这段文档测试。
不过不建议滥用,因为这会失去文档测试的价值。
标记代码不需要运行
还可以使用:
csharp
/// ```no_run
/// let server = create_server();
/// server.run();
/// ```
pub fn start_server() {
}
no_run 表示:
-
代码需要通过编译
-
但不实际运行
适合:
-
启动服务器
-
连接数据库
-
删除文件
-
调用远程接口
-
长时间运行任务
普通注释与文档注释对比
| 类型 | 语法 | 主要用途 |
|---|---|---|
| 单行注释 | // |
解释代码逻辑 |
| 块注释 | /* */ |
多行说明或临时注释代码 |
| 项目文档注释 | /// |
为函数、结构体、枚举等生成文档 |
| 模块文档注释 | //! |
为模块或 Crate 生成文档 |
TODO 注释
开发中经常会写:
arduino
// TODO: 增加参数校验
表示这里还有功能需要完成。
例如:
rust
fn create_user(name: &str) {
// TODO: 检查用户名是否为空
println!("创建用户:{}", name);
}
还可以使用:
arduino
// FIXME: 当前实现在线程环境下可能有问题
表示这里存在需要修复的问题。
不过,这类注释应该配合任务管理系统使用,避免永久留在代码中。
不推荐的注释方式
重复代码含义
不推荐:
arduino
// 把 count 加 1
count += 1;
因为代码本身已经非常清楚。
更有价值的注释应该说明原因:
arduino
// 跳过表头,因此从第二行开始统计
count += 1;
注释与代码不一致
例如:
ini
// 默认端口为 8080
let port = 3000;
这比没有注释更加危险。
维护代码时必须同步更新注释。
注释掉大量旧代码
不推荐:
arduino
/*
fn old_login() {
// 几十行旧代码
}
*/
旧代码应该交给 Git 保存。
源码中只保留当前有效实现。
给每一行都写注释
不推荐:
ini
// 定义 a
let a = 10;
// 定义 b
let b = 20;
// 计算结果
let result = a + b;
// 输出结果
println!("{}", result);
这种注释会让代码显得冗余。
变量名和函数名足够清楚时,不需要逐行解释。
注释最佳实践
优先写清晰的代码
例如:
不推荐:
ini
let x = p * q;
然后写:
arduino
// 商品单价乘以数量得到总价
更推荐直接改成:
ini
let total_price = unit_price * quantity;
清晰命名往往比注释更有效。
解释为什么,而不是解释是什么
不推荐:
less
// 等待 3 秒
sleep(Duration::from_secs(3));
更推荐:
less
// 第三方接口存在短时间限流,重试前等待 3 秒
sleep(Duration::from_secs(3));
公共 API 尽量编写文档注释
例如:
csharp
pub fn calculate_total() {
}
如果这是对外使用的函数,应该补充:
-
功能
-
参数
-
返回值
-
错误
-
示例
这样其他开发者不需要阅读函数内部实现,就能正确使用它。
文档示例尽量可运行
文档中的代码最好能够通过:
bash
cargo test
这样可以避免文档长期失效。
实战:为订单模块编写文档
下面是一个完整示例:
rust
//! 订单金额计算模块。
//!
//! 提供商品小计、折扣金额和最终订单金额计算功能。
/// 计算商品小计。
///
/// # Arguments
///
/// - `price`:商品单价
/// - `quantity`:商品数量
///
/// # Returns
///
/// 返回商品单价与数量相乘后的结果。
///
/// # Examples
///
/// ```
/// let subtotal = 29.9 * 3.0;
/// assert_eq!(subtotal, 89.7);
/// ```
pub fn calculate_subtotal(price: f64, quantity: u32) -> f64 {
price * quantity as f64
}
/// 根据折扣率计算折后金额。
///
/// # Arguments
///
/// - `subtotal`:商品小计
/// - `discount_rate`:折扣率,范围为 `0.0..=1.0`
///
/// # Returns
///
/// 当折扣率有效时返回折后金额,否则返回原始小计。
///
/// # Examples
///
/// ```
/// let result = 100.0 * 0.8;
/// assert_eq!(result, 80.0);
/// ```
pub fn apply_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 格式
-
Examples、Errors、Panics和Safety -
使用
cargo doc生成文档 -
使用
cargo doc --no-deps --open打开项目文档 -
文档测试 Doctest
-
ignore和no_run -
注释常见误区和最佳实践
最重要的原则是:
代码负责表达"做什么",注释负责解释"为什么"。
清晰的代码配合准确的文档,才能让一个 Rust 项目真正具备长期维护价值。
下一篇预告
下一篇我们将学习 Rust 控制流之 if 条件判断,包括:
-
if、else if和else -
比较运算符
-
逻辑运算符
-
if表达式 -
条件分支返回值
-
常见类型错误
-
实战:根据用户等级计算折扣