Skip to content

文档、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」达到同样效果。这在 stdtokio 里大量存在(例如 tokio::io::AsyncRead 的 sealed 内部扩展)。



代码质量与 lint

cargo fmtcargo 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::allcorrectness + style + complexity + perf默认开启,全项目 warndeny
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



内容以 rustc 1.98.1 · Rust 2024 edition 为基准