文档、API 设计与代码质量
文档注释即测试、API 设计准则与 clippy 纪律。
文档与 API 设计
/// 文档注释与 doctest
文档注释里的代码块会被 cargo test 真的编译并执行,这就是 doctest。它同时是「文档」和「测试」,因此不会过期。
rust
/// 把摄氏度转成华氏度。
///
/// # Examples
///
/// ```
/// use mylib::celsius_to_fahrenheit;
/// assert_eq!(celsius_to_fahrenheit(0.0), 32.0);
/// assert_eq!(celsius_to_fahrenheit(100.0), 212.0);
/// ```
///
/// # Panics
///
/// 输入为 `NaN` 时不会 panic,而是返回 `NaN`。
///
/// # Errors
///
/// 本函数不会返回错误。
///
/// # Safety
///
/// 本函数是安全函数,第 「unsafe 与 FFI 入门(只讲原则)」一节的 `unsafe fn` 才需要本节。
#[must_use]
pub fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}doctest 的扩展标记:
| 标记 | 作用 |
|---|---|
``` | 会编译 + 运行,失败即测试失败 |
```no_run | 编译但不运行(如要连数据库、读文件的例子) |
```ignore | 完全不处理(尽量避免,会腐化) |
```compile_fail | 断言「这段代码不能编译」(教借用检查器时极有用) |
```text | 纯文本,不涉及 Rust 代码 |
# 行首 | 该行在渲染出的文档里隐藏,但仍参与编译 |
紧凑示例(隐藏样板,让文档干净):
rust
演示用 `# ` 隐藏 `fn main` 样板。
///
/// ```
/// # fn main() -> Result<(), std::num::ParseIntError> {
/// let n: i32 = "42".parse()?;
/// assert_eq!(n, 42);
/// # Ok(())
/// # }
/// ```
pub fn placeholder() {}常用命令:
powershell
cargo doc --open # 生成并打开 HTML 文档
cargo doc --no-deps # 只给本项目(不看依赖)出文档,快
cargo test --doc # 只跑 doctest
cargo test # 单元 + 集成 + doctest 全跑crate 级文档与 include_str! README
rust
//! # mylib
//!
//! 一句话说明这个 crate 做什么。
//!
//! ## 快速开始
//!
//! ```
//! let v = mylib::double(21);
//! assert_eq!(v, 42);
//! ```
//!
//! ## feature
//!
//! - `json`:启用 serde 支持。
#![warn(missing_docs)] // 公开项缺文档即警告
// README 只维护一份:doc 里直接内联,避免两边不同步
#![doc = include_str!("../README.md")]
/// 把整数翻倍。
pub fn double(x: i32) -> i32 { x * 2 }⚠️ 陷阱:
include_str!("../README.md")的路径相对于当前源文件所在目录,不是相对 crate 根,也不是相对Cargo.toml。所以src/lib.rs里是../README.md;若写在src/net/mod.rs里就是../../README.md。此外 README 里的相对链接与图片在 doc 里会失效,且 README 中的代码块会被当 doctest 执行——务必保证 README 的示例可编译(或给它们加```text/ignore)。
#[doc(hidden)] 用于「公开但不想让人用」的条目:宏展开产生的辅助项、serde 的 __private 类型、为兼容而保留的旧 API。
rust
/// 不要直接使用:仅用于宏展开。
#[doc(hidden)]
pub struct __PrivateToken {
_priv: (),
}API 设计准则
参数接受 &str / &[T] 而非 &String / &Vec<T>
rust
/// 好:调用方传 &str、&String、&'static str 都行(deref coercion)
pub fn greet(name: &str) -> String { format!("Hello, {name}!") }
/// 好:切片比 &Vec<T> 更通用,&Vec<T> 和 &[T; N] 都能传进来
pub fn sum(xs: &[i32]) -> i32 { xs.iter().sum() }
/// 更好:需要「可能拥有」时用 impl Into<String> / AsRef<str>
pub fn tagged(name: impl Into<String>, tags: &[&str]) -> (String, usize) {
(name.into(), tags.len())
}
fn demo() {
let owned = String::from("Ada");
let _ = greet(&owned); // &String -> &str 自动转换
let _ = greet("Grace");
let arr = [1, 2, 3];
let _ = sum(&arr); // 数组也能传
let _ = sum(&vec![1, 2, 3]);
let _ = tagged("Ada", &["admin"]);
}实现 From / FromStr / TryFrom,让类型融入标准生态
rust
/// 版本号字符串,形如 "1.2.3"。
#[derive(Debug, PartialEq)]
pub struct Version(u32, u32, u32);
impl std::str::FromStr for Version {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut it = s.split('.');
let (a, b, c) = (it.next(), it.next(), it.next());
match (a, b, c, it.next()) {
(Some(a), Some(b), Some(c), None) => Ok(Version(
a.parse().map_err(|_| format!("非法主版本 {a}"))?,
b.parse().map_err(|_| format!("非法次版本 {b}"))?,
c.parse().map_err(|_| format!("非法补丁 {c}"))?,
)),
_ => Err(format!("需要三段式版本号,收到 `{s}`")),
}
}
}
impl From<(u32, u32, u32)> for Version {
fn from(t: (u32, u32, u32)) -> Self { Version(t.0, t.1, t.2) }
}
fn main() {
let v: Version = "1.2.3".parse().expect("示例输入合法");
assert_eq!(v, Version::from((1, 2, 3)));
println!("{v:?}");
}实现了 FromStr 就能用 str::parse();实现了 From<T> 就自动获得 TryFrom<T> 与 T::into()。From 而非 Into 是给别人的礼物:写 From 你两边都得到。
返回 impl Iterator 而不是具体容器
rust
/// 返回 impl Iterator:不暴露内部容器类型,未来把 Vec 换成别的结构不算破坏性变更。
pub fn evens(xs: &[i32]) -> impl Iterator<Item = i32> + '_ {
xs.iter().copied().filter(|x| x % 2 == 0)
}
fn main() {
let v: Vec<i32> = evens(&[1, 2, 3, 4]).collect();
assert_eq!(v, [2, 4]);
println!("{v:?}");
}⚠️ 陷阱:2024 edition 中
impl Trait默认捕获全部在签名里出现的生命周期参数,所以+ '_往往可以省略;但如果你显式写了+ '_或+ 'a,在 2021 与 2024 下行为一致。反过来,从 2021 迁移到 2024 时,原来编译不过的「捕获过多」场景现在能过,原来能过的「隐式不捕获」场景可能需要+ use<>来收紧——见 RPIT capture rules。
#[must_use]:把「忽略返回值是 bug」表达成类型系统能检查的东西。
rust
/// 计算校验和。调用方必须使用结果。
#[must_use = "校验和必须被使用,否则计算无意义"]
pub fn checksum(data: &[u8]) -> u32 {
data.iter().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(u32::from(*b)))
}
/// 建造者未 build 时提醒。
#[must_use = "Builder 不调用 build() 不会有任何效果"]
pub struct ReportBuilder {
title: String,
}
impl ReportBuilder {
/// 新建。
pub fn new(title: impl Into<String>) -> Self { Self { title: title.into() } }
/// 生成最终报告。
pub fn build(self) -> String { format!("# {}", self.title) }
}Builder 模式:可选参数多、又不想引入重载时使用。#[derive(Default)] + ..Default::default() 是配套技巧。
rust
/// 服务器配置建造者。
#[derive(Debug, Default, Clone)]
pub struct ServerConfig {
host: String,
port: u16,
workers: usize,
}
impl ServerConfig {
/// 起步配置:host 必填,其余取合理默认。
#[must_use]
pub fn new(host: impl Into<String>) -> Self {
Self { host: host.into(), port: 8080, workers: 4 }
}
/// 设置端口。
#[must_use]
pub fn port(mut self, port: u16) -> Self { self.port = port; self }
/// 设置工作线程数。
#[must_use]
pub fn workers(mut self, n: usize) -> Self { self.workers = n; self }
}
fn main() {
// 链式 + ..Default::default() 让新增字段不破坏旧调用
let cfg = ServerConfig { port: 9000, ..ServerConfig::new("0.0.0.0") };
println!("{cfg:?}");
}sealed trait 模式:想让外部能使用某个 trait 但不能实现它。公开 trait 的实现是兼容性承诺(加一个必需方法就是破坏性变更),sealed 能保留以后扩展的自由。
rust
/// 只有本 crate 能实现的 trait:外部可作泛型约束使用,但无法 impl。
pub trait Shape: private::Sealed {
/// 面积。
fn area(&self) -> f64;
}
mod private {
/// 私有标记 trait,外部无法命名,因而无法实现。
pub trait Sealed {}
}
/// 圆。
pub struct Circle {
/// 半径。
pub radius: f64,
}
impl private::Sealed for Circle {}
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
/// 泛型消费端:接受任何 Shape。
pub fn describe<S: Shape>(s: &S) -> String {
format!("面积 {:.2}", s.area())
}
fn main() {
println!("{}", describe(&Circle { radius: 2.0 }));
}💡 对照:Java 里用
sealed interface+permits从语言层面限制子类;Rust 没有permits,于是用「私有 supertrait」达到同样效果。这在std与tokio里大量存在(例如tokio::io::AsyncRead的 sealed 内部扩展)。
代码质量与 lint
cargo fmt 与 cargo clippy
powershell
rustup component add rustfmt clippy # 若未随工具链安装
cargo fmt # 格式化全部
cargo fmt --check # CI:只检查,不改文件(有差异即失败)
cargo fmt -- --config style_edition=2024 # 临时指定风格版
cargo clippy # 跑默认 lint(correctness 等)
cargo clippy --all-targets --all-features # CI 推荐:连带测试、bench、所有 feature
cargo clippy -- -D warnings # 所有警告升级为错误
cargo clippy --fix --allow-dirty # 自动修可机械修复的部分clippy 的 lint 分组与取舍:
| 分组 | 内容 | 建议 |
|---|---|---|
clippy::correctness | 明确写错的代码(默认 deny) | 必须修,不要 allow |
clippy::all | correctness + style + complexity + perf | 默认开启,全项目 warn 或 deny |
clippy::pedantic | 更严格风格与 API 建议,含不少个人偏好 | 新项目可开 warn 再逐条 allow;老项目逐步引入 |
clippy::nursery | 实验性 lint,可能误报、可能改名 | 不进 CI 主线,只本地参考 |
clippy::restriction | 「必须显式选择」的极端规则 | 只挑单条 lint 用,绝不整组启用 |
#[allow] 的正确用法
原则:允许范围尽量小,并且必须写理由。
rust
// ❌ 坏:crate 级一刀切,掩盖了未来所有同类问题
#![allow(clippy::all)]
// ✅ 好:精确到单个函数 + 单个 lint + 说明原因
#[allow(clippy::too_many_arguments)] // 这里是 FFI 回调签名,参数由 C 端固定,不能改
extern "C" fn callback(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32) -> i32 {
a + b + c + d + e + f + g + h
}
// ✅ 好:为「教学示例」或「生成代码」局部放行,并用 expect 记录假设
#[expect(clippy::needless_range_loop, reason = "索引同时用于两个数组,用迭代器反而更难读")]
fn zip_by_index(a: &[i32], b: &[i32]) -> Vec<i32> {
let mut out = Vec::new();
for i in 0..a.len().min(b.len()) {
out.push(a[i] + b[i]);
}
out
}
fn main() { println!("{}", callback(1, 2, 3, 4, 5, 6, 7, 8)); }🚀 进阶:优先用
#[expect(lint, reason = "...")](Rust 1.81 稳定)而不是#[allow]。expect会在「该 lint 其实已经不再触发」时反过来警告你,从而自动清理掉过期的放行;allow则会静默烂在那里。
[lints] 表(Cargo 1.74+)
把 lint 配置从源码里的 #![...] 搬到 Cargo.toml,好处是一处集中、可被 workspace 继承:
toml
# 根 Cargo.toml(workspace)
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "deny" # edition 2024 默认 warn,这里升级为 deny
missing_docs = "warn"
unused_must_use = "deny"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 } # priority 越小越先应用,供后续覆盖
pedantic = { level = "warn", priority = -1 }
# 在 pedantic 基础上关掉不想要的单条
module_name_repetitions = "allow"
must_use_candidate = "allow"toml
# 成员 crate 的 Cargo.toml:一行继承
[lints]
workspace = true🧠 原理:
priority决定同一条 lint 被多条规则命中时谁生效(数值小的先应用,数值大的覆盖它)。默认 priority 为 0,因此把整组设为-1、单条设为0(或直接写"allow")就能实现「整组开、单条关」。
unsafe_op_in_unsafe_fn 与 CI 的 -D warnings
rust
// 2024 edition 中该 lint 默认 warn;crate 根显式 deny 可防止团队内回退
#![deny(unsafe_op_in_unsafe_fn)]
/// 读取切片第 i 个元素,不做边界检查。
///
/// # Safety
///
/// 调用者必须保证 `i < x.len()`。
pub unsafe fn get_unchecked_i32(x: &[i32], i: usize) -> i32 {
// 必须显式 unsafe 块:让「哪些操作不安全」在代码里可见
unsafe { *x.get_unchecked(i) }
}
fn main() {
let v = [1, 2, 3];
// 调用 unsafe fn 本身也需要 unsafe 块,并且要满足 Safety 契约
let x = unsafe { get_unchecked_i32(&v, 1) };
assert_eq!(x, 2);
}CI 里的 -D warnings 写法:
powershell
cargo clippy --all-targets --all-features -- -D warnings⚠️ 陷阱:
RUSTFLAGS="-D warnings"会让依赖也受影响,导致「上游某个 crate 的新版本引入警告 → 你的 CI 突然红」。正确做法是把-D warnings只加在自己的clippy/check调用参数上(如上),或使用--config 'build.rustflags=[]'精确控制,别用全局RUSTFLAGS。