Rust 模式与工程实践指南
演讲者介绍
- 微软 SCHIE(硅和云硬件基础设施工程)团队首席固件架构师
- 行业资深专家,专长于安全、系统编程(固件、操作系统、管理程序)、CPU 和平台架构,以及 C++ 系统
- 2017 年开始使用 Rust 编程(@AWS EC2),从此爱上了这门语言
这是一本关于 Rust 模式中高级模式的实用指南,这些模式出现在真实的代码库中。这不是语言教程 —— 它假设你会编写基本的 Rust 代码并想要提升水平。每一章隔离一个概念,解释何时以及为何使用它,并提供可编译的示例和内联练习。
本书适合谁
- 已完成 The Rust Programming Language 但在“如何实际设计“上遇到困难的开发者
- 将生产系统翻译成 Rust 的 C++/C# 工程师
- 任何在 generics、trait bounds 或 lifetime 错误上遇到瓶颈,想要系统化工具包的人
前置要求
开始前,你应该熟悉:
- 所有权、借用和生命周期(基础水平)
- 枚举、模式匹配和
Option/Result - 结构体、方法和基础 traits(
Display、Debug、Clone) - Cargo 基础:
cargo build、cargo test、cargo run
如何使用本书
难度图例
每章都标有难度级别:
| 符号 | 级别 | 含义 |
|---|---|---|
| 🟢 | 基础 | 每个 Rust 开发者都需要掌握的核心概念 |
| 🟡 | 中级 | 生产代码库中使用的模式 |
| 🔴 | 高级 | 深入的语言机制 —— 根据需要重温 |
节奏指南
| 章节 | 主题 | 建议时间 | 检查点 |
|---|---|---|---|
| 第一部分:类型级模式 | |||
| 1. Generics 🟢 | 单态化、const generics、const fn | 1-2 小时 | 能解释何时 dyn Trait 优于 generics |
| 2. Traits 🟡 | 关联类型、GATs、blanket impls、vtables | 3-4 小时 | 能设计带关联类型的 trait |
| 3. Newtype 和 Type-State 🟡 | 零成本安全、编译时 FSMs | 2-3 小时 | 能构建 type-state builder 模式 |
| 4. PhantomData 🔴 | 生命周期标记、variance、drop check | 2-3 小时 | 能解释为何 PhantomData<fn(T)> 不同于 PhantomData<T> |
| 第二部分:并发与运行时 | |||
| 5. Channels 🟢 | mpsc、crossbeam、select!、actors | 1-2 小时 | 能实现基于 channel 的工作池 |
| 6. Concurrency 🟡 | 线程、rayon、Mutex、RwLock、atomics | 2-3 小时 | 能为场景选择正确的同步原语 |
| 7. Closures 🟢 | Fn/FnMut/FnOnce、组合器 | 1-2 小时 | 能编写接受闭包的高阶函数 |
| 8. 函数式 vs 命令式 🟡 | 组合器、迭代器适配器、函数式模式 | 2-3 小时 | 能解释何时函数式风格优于命令式 |
| 9. 智能指针 🟡 | Box、Rc、Arc、RefCell、Cow、Pin | 2-3 小时 | 能解释何时使用每种智能指针 |
| 第三部分:系统与生产 | |||
| 10. 错误处理 🟢 | thiserror、anyhow、? 操作符 | 1-2 小时 | 能设计错误类型层次结构 |
| 11. 序列化 🟡 | serde、零拷贝、二进制数据 | 2-3 小时 | 能编写自定义 serde 反序列化器 |
| 12. Unsafe 🔴 | 超能力、FFI、UB 陷阱、分配器 | 2-3 小时 | 能将 unsafe 代码包装在可靠的 safe API 中 |
| 13. Macros 🟡 | macro_rules!、proc macros、syn/quote | 2-3 小时 | 能编写带 tt 咀嚼的声明式宏 |
| 14. Testing 🟢 | 单元/集成/doc 测试、proptest、criterion | 1-2 小时 | 能设置基于属性的测试 |
| 15. API 设计 🟡 | 模块布局、 ergonomic APIs、feature flags | 2-3 小时 | 能应用“parse, don’t validate“模式 |
| 16. Async 🔴 | Futures、Tokio、常见陷阱 | 1-2 小时 | 能识别 async 反模式 |
| 附录 | |||
| 参考卡 | 快速查阅 trait bounds、lifetimes、模式 | 按需使用 | — |
| 综合项目 | 类型安全任务调度器 | 4-6 小时 | 提交一个可工作的实现 |
总估计时间:通过练习深入学习需要 30-45 小时。
完成练习
每章都以实践练习结束。为了最大化学习效果:
- 先自己尝试 —— 在查看答案前至少花 15 分钟
- 手写代码 —— 不要复制粘贴;手写建立肌肉记忆
- 修改解决方案 —— 添加功能、改变约束、故意破坏一些东西
- 检查交叉引用 —— 大多数练习结合了来自多章的模式
附录中的综合项目将全书的模式整合到一个单一的、生产质量的系统中。
目录
第一部分:类型级模式
1. Generics — 全貌 🟢
单态化、代码膨胀权衡、generics vs enums vs trait objects、const generics、const fn。
2. Traits 深入 🟡 关联类型、GATs、blanket impls、marker traits、vtables、HRTBs、扩展 traits、枚举分发。
3. Newtype 和 Type-State 模式 🟡 零成本类型安全、编译时状态机、builder 模式、配置 traits。
4. PhantomData — 不携带数据的类型 🔴 生命周期标记、度量单位模式、drop check、variance。
第二部分:并发与运行时
5. Channels 和消息传递 🟢
std::sync::mpsc、crossbeam、select!、背压、actor 模式。
6. 并发 vs 并行 vs 线程 🟡 OS 线程、scoped threads、rayon、Mutex/RwLock/Atomics、Condvar、OnceLock、无锁模式。
7. 闭包和高阶函数 🟢
Fn/FnMut/FnOnce、闭包作为参数/返回值、组合器、高阶 APIs。
8. 函数式 vs 命令式:何时优雅获胜(何时不会) 🟡 组合器、迭代器适配器、函数式模式。
9. 智能指针和内部可变性 🟡 Box、Rc、Arc、Weak、Cell/RefCell、Cow、Pin、ManuallyDrop。
第三部分:系统与生产
10. 错误处理模式 🟢
thiserror vs anyhow、#[from]、.context()、? 操作符、panics。
11. 序列化、零拷贝和二进制数据 🟡
serde 基础、枚举表示、零拷贝反序列化、repr(C)、bytes::Bytes。
12. Unsafe Rust — 受控的危险 🔴 五种超能力、可靠的抽象、FFI、UB 陷阱、arena/slab 分配器。
13. Macros — 编写代码的代码 🟡
macro_rules!、何时(不)使用 macros、proc macros、derive macros、syn/quote。
14. 测试和基准测试模式 🟢 单元/集成/doc 测试、proptest、criterion、mock 策略。
15. Crate 架构和 API 设计 🟡 模块布局、API 设计检查清单、ergonomic 参数、feature flags、workspaces。
16. Async/Await 要点 🔴 Futures、Tokio 快速入门、常见陷阱。(深入 async 覆盖,请参阅我们的 Async Rust 培训。)
附录
总结和参考卡 模式决策指南、trait bounds 速查表、生命周期省略规则、进一步阅读。
综合项目:类型安全任务调度器 将 generics、traits、typestate、channels、错误处理和测试集成到一个完整的系统中。
1. Generics — 全貌 🟢
你将学到:
- 单态化如何提供零成本 generics —— 以及何时导致代码膨胀
- 决策框架:generics vs enums vs trait objects
- Const generics 用于编译时数组大小,
const fn用于编译时求值- 何时在冷路径上用静态dispatch 换取动态 dispatch
单态化与零成本
Rust 中的 generics 是单态化的 —— 编译器为每个 generic 函数生成一份专门化的副本,针对它所使用的每个具体类型。这与 Java/C# 相反,后者的 generics 在运行时被擦除。
fn max_of<T: PartialOrd>(a: T, b: T) -> T {
if a >= b { a } else { b }
}
fn main() {
max_of(3_i32, 5_i32); // 编译器生成 max_of_i32
max_of(2.0_f64, 7.0_f64); // 编译器生成 max_of_f64
max_of("a", "z"); // 编译器生成 max_of_str
}
编译器实际生成的内容(概念上):
#![allow(unused)]
fn main() {
// 三个独立的函数 —— 无运行时 dispatch,无 vtable:
fn max_of_i32(a: i32, b: i32) -> i32 { if a >= b { a } else { b } }
fn max_of_f64(a: f64, b: f64) -> f64 { if a >= b { a } else { b } }
fn max_of_str<'a>(a: &'a str, b: &'a str) -> &'a str { if a >= b { a } else { b } }
}
为什么
max_of_str需要<'a>而max_of_i32不需要?i32和f64是Copy类型 —— 函数返回一个自有值。但&str是引用, 所以编译器必须知道返回引用的生命周期。<'a>注解 表示“返回的&str存活时间至少与两个输入一样长“。
优势:零运行时成本 —— 与手写的专门化代码相同。优化器可以内联、向量化,并独立专门化每个副本。
与 C++ 对比:Rust generics 像 C++ 模板一样工作,但有一个关键区别 —— bounds 检查发生在定义时,而非实例化时。在 C++ 中,模板仅在使用特定类型时才编译,导致库代码深处出现晦涩的错误消息。在 Rust 中,T: PartialOrd 在定义函数时就被检查,所以错误被及早发现,消息清晰。
#![allow(unused)]
fn main() {
// Rust:在定义位置报错 —— "T doesn't implement Display"
fn broken<T>(val: T) {
println!("{val}"); // ❌ Error: T doesn't implement Display
}
}
#![allow(unused)]
fn main() {
// 修复:添加 bound
fn fixed<T: std::fmt::Display>(val: T) {
println!("{val}"); // ✅
}
}
当 Generics 有害时:代码膨胀
单态化有代价 —— 二进制大小。每个唯一的实例化都会复制函数体:
// 这个无辜的函数...
fn serialize<T: serde::Serialize>(value: &T) -> Vec<u8> {
serde_json::to_vec(value).unwrap()
}
// ...用于 50 个不同类型 → 二进制中 50 份副本。
缓解策略:
// 1. 提取非 generic 核心("outline" 模式)
fn serialize<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
// Generic 部分:仅序列化调用
let json_value = serde_json::to_value(value)?;
// 非 Generic 部分:提取到独立函数
serialize_value(json_value)
}
fn serialize_value(value: serde_json::Value) -> Result<Vec<u8>, serde_json::Error> {
// 这个函数在二进制中只存在一次
serde_json::to_vec(&value)
}
// 2. 当内联不关键时使用 trait objects(动态 dispatch)
fn log_item(item: &dyn std::fmt::Display) {
// 一份副本 —— 使用 vtable 进行 dispatch
println!("[LOG] {item}");
}
经验法则:对内联重要的热路径使用 generics。 对冷路径(错误处理、日志、配置)使用
dyn Trait, vtable 调用可忽略不计。
Generics vs Enums vs Trait Objects — 决策指南
Rust 中处理“不同类型,相同接口“的三种方式:
| 方法 | Dispatch | 已知于 | 可扩展? | 开销 |
|---|---|---|---|---|
Generics(impl Trait / <T: Trait>) | 静态(单态化) | 编译时 | ✅(开放集) | 零 —— 可内联 |
| Enum | Match 分支 | 编译时 | ❌(封闭集) | 零 —— 无 vtable |
Trait object(dyn Trait) | 动态(vtable) | 运行时 | ✅(开放集) | Vtable 指针 + 间接调用 |
// --- GENERICS:开放集,零成本,编译时 ---
fn process<H: Handler>(handler: H, request: Request) -> Response {
handler.handle(request) // 单态化 —— 每个 H 一份副本
}
// --- ENUM:封闭集,零成本,穷尽匹配 ---
enum Shape {
Circle(f64),
Rect(f64, f64),
Triangle(f64, f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect(w, h) => w * h,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
}
// 添加新变体强制更新所有 match 分支 —— 编译器
// 强制穷尽性。适合"我控制所有变体"的情况。
// --- TRAIT OBJECT:开放集,运行时成本,可扩展 ---
fn log_all(items: &[Box<dyn std::fmt::Display>]) {
for item in items {
println!("{item}"); // vtable dispatch
}
}
决策流程图:
flowchart TD
A["编译时知道所有<br>可能类型?"]
A -->|"是,小的<br>封闭集"| B["Enum"]
A -->|"是,但集<br>开放"| C["Generics<br>(单态化)"]
A -->|"否 —— 类型<br>运行时决定"| D["dyn Trait"]
C --> E{"热路径?<br>(百万次调用)"}
E -->|是 | F["Generics<br>(可内联)"]
E -->|否 | G["dyn Trait<br>可以"]
D --> H{"需要在同一<br>集合中混合类型?"}
H -->|是 | I["Vec<Box<dyn Trait>>"]
H -->|否 | C
style A fill:#e8f4f8,stroke:#2980b9,color:#000
style B fill:#d4efdf,stroke:#27ae60,color:#000
style C fill:#d4efdf,stroke:#27ae60,color:#000
style D fill:#fdebd0,stroke:#e67e22,color:#000
style F fill:#d4efdf,stroke:#27ae60,color:#000
style G fill:#fdebd0,stroke:#e67e22,color:#000
style I fill:#fdebd0,stroke:#e67e22,color:#000
style E fill:#fef9e7,stroke:#f1c40f,color:#000
style H fill:#fef9e7,stroke:#f1c40f,color:#000
Const Generics
自 Rust 1.51 起,你可以在常数值上参数化类型和函数,而不仅是类型:
#![allow(unused)]
fn main() {
// 数组包装器,按大小参数化
struct Matrix<const ROWS: usize, const COLS: usize> {
data: [[f64; COLS]; ROWS],
}
impl<const ROWS: usize, const COLS: usize> Matrix<ROWS, COLS> {
fn new() -> Self {
Matrix { data: [[0.0; COLS]; ROWS] }
}
fn transpose(&self) -> Matrix<COLS, ROWS> {
let mut result = Matrix::<COLS, ROWS>::new();
for r in 0..ROWS {
for c in 0..COLS {
result.data[c][r] = self.data[r][c];
}
}
result
}
}
// 编译器强制执行维度正确性:
fn multiply<const M: usize, const N: usize, const P: usize>(
a: &Matrix<M, N>,
b: &Matrix<N, P>, // N 必须匹配!
) -> Matrix<M, P> {
let mut result = Matrix::<M, P>::new();
for i in 0..M {
for j in 0..P {
for k in 0..N {
result.data[i][j] += a.data[i][k] * b.data[k][j];
}
}
}
result
}
// 用法:
let a = Matrix::<2, 3>::new(); // 2×3
let b = Matrix::<3, 4>::new(); // 3×4
let c = multiply(&a, &b); // 2×4 ✅
// let d = Matrix::<5, 5>::new();
// multiply(&a, &d); // ❌ 编译错误:期望 Matrix<3, _>,得到 Matrix<5, 5>
}
C++ 对比:这类似于 C++ 中的
template<int N>,但 Rust const generics 被急切地类型检查,不受 SFINAE 复杂性影响。
Const 函数(const fn)
const fn 标记一个函数可在编译时求值 —— Rust 的 C++ constexpr 等价物。结果可用于 const 和 static 上下文:
#![allow(unused)]
fn main() {
// 基础 const fn —— 在 const 上下文中编译时求值
const fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
const BOILING_F: f64 = celsius_to_fahrenheit(100.0); // 编译时计算
const FREEZING_F: f64 = celsius_to_fahrenheit(0.0); // 32.0
// Const 构造函数 —— 创建 statics 无需 lazy_static!
struct BitMask(u32);
impl BitMask {
const fn new(bit: u32) -> Self {
BitMask(1 << bit)
}
const fn or(self, other: BitMask) -> Self {
BitMask(self.0 | other.0)
}
const fn contains(&self, bit: u32) -> bool {
self.0 & (1 << bit) != 0
}
}
// 静态查找表 —— 无运行时成本,无惰性初始化
const GPIO_INPUT: BitMask = BitMask::new(0);
const GPIO_OUTPUT: BitMask = BitMask::new(1);
const GPIO_IRQ: BitMask = BitMask::new(2);
const GPIO_IO: BitMask = GPIO_INPUT.or(GPIO_OUTPUT);
// 寄存器映射作为 const 数组:
const SENSOR_THRESHOLDS: [u16; 4] = {
let mut table = [0u16; 4];
table[0] = 50; // 警告
table[1] = 70; // 高
table[2] = 85; // 关键
table[3] = 100; // 关闭
table
};
// 整个表存在于二进制中 —— 无堆,无运行时初始化。
}
在 const fn 中可以做什么(截至 Rust 1.79+):
- 算术、位操作、比较
if/else、match、loop、while(控制流)- 创建和修改局部变量(
let mut) - 调用其他
const fns - 引用(
&、&mut—— 在 const 上下文内) panic!()(如果编译时触发则成为编译错误)- 基础浮点算术(
+、-、*、/;复杂操作如sqrt/sin不符合 const 条件)
不能做什么(还):
- 堆分配(
Box、Vec、String) - Trait 方法调用(仅固有方法)
- I/O 或副作用
#![allow(unused)]
fn main() {
// 带 panic 的 const fn —— 在编译时成为编译错误:
const fn checked_div(a: u32, b: u32) -> u32 {
if b == 0 {
panic!("division by zero"); // 如果 b 在 const 时为 0 则是编译错误
}
a / b
}
const RESULT: u32 = checked_div(100, 4); // ✅ 25
// const BAD: u32 = checked_div(100, 0); // ❌ 编译错误:"division by zero"
}
C++ 对比:
const fn是 Rust 的constexpr。关键区别: Rust 的版本是选择加入的,编译器严格验证只使用 const 兼容操作。 在 C++ 中,constexpr函数可以静默回退到运行时求值 —— 在 Rust 中,const上下文要求编译时求值,否则是硬错误。
实用建议:尽可能使构造函数和简单工具函数成为
const fn—— 它没有成本,并允许调用者在 const 上下文中使用它们。对于硬件诊断代码,const fn非常适合寄存器定义、位掩码构建和阈值表。
关键要点 —— Generics
- 单态化提供零成本抽象,但可能导致代码膨胀 —— 对冷路径使用
dyn Trait- Const generics(
[T; N])用编译时检查的数组大小替代 C++ 模板技巧const fn消除了lazy_static!,用于编译时可计算的值
另见: 第 2 章 — Traits 深入 了解 trait bounds、关联类型和 trait objects。第 4 章 — PhantomData 了解零大小 generic 标记。
练习:带淘汰的 Generic Cache ★★(约 30 分钟)
构建一个 generic 的 Cache<K, V> 结构体,存储键值对,具有可配置的最大容量。满时淘汰最旧的条目(FIFO)。要求:
fn new(capacity: usize) -> Selffn insert(&mut self, key: K, value: V)—— 达到容量时淘汰最旧的fn get(&self, key: &K) -> Option<&V>fn len(&self) -> usize- 约束
K: Eq + Hash + Clone
🔑 解答
use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
struct Cache<K, V> {
map: HashMap<K, V>,
order: VecDeque<K>,
capacity: usize,
}
impl<K: Eq + Hash + Clone, V> Cache<K, V> {
fn new(capacity: usize) -> Self {
Cache {
map: HashMap::with_capacity(capacity),
order: VecDeque::with_capacity(capacity),
capacity,
}
}
fn insert(&mut self, key: K, value: V) {
if self.capacity == 0 {
// 无容量!
return;
}
if self.map.contains_key(&key) {
self.map.insert(key, value);
return;
}
if self.map.len() >= self.capacity {
if let Some(oldest) = self.order.pop_front() {
self.map.remove(&oldest);
}
}
self.order.push_back(key.clone());
self.map.insert(key, value);
}
fn get(&self, key: &K) -> Option<&V> {
self.map.get(key)
}
fn len(&self) -> usize {
self.map.len()
}
}
fn main() {
// 基础缓存测试
let mut cache = Cache::new(3);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
assert_eq!(cache.len(), 3);
cache.insert("d", 4); // 淘汰 "a"
assert_eq!(cache.get(&"a"), None);
assert_eq!(cache.get(&"d"), Some(&4));
// 留给读者:`capacity` 属性应该是什么类型,
// 以确保无法定义这种无用的缓存?
let mut empty_cache = Cache::new(0);
empty_cache.insert("0", 0);
assert_eq!(empty_cache.get(&"0"), None);
assert_eq!(empty_cache.len(), 0);
println!("Cache works! len = {}", cache.len());
}
2. Traits 深入 🟡
你将学到:
- 关联类型 vs generic 参数 —— 以及何时使用每种
- GATs、blanket impls、marker traits 和 trait object 安全规则
- vtables 和 fat pointers 如何在底层工作
- 扩展 traits、enum dispatch 和 typed command 模式
关联类型 vs Generic 参数
两者都让 trait 可以与不同类型一起工作,但服务于不同目的:
#![allow(unused)]
fn main() {
// --- 关联类型:每个类型一个实现 ---
trait Iterator {
type Item; // 每个迭代器恰好产生一种 item
fn next(&mut self) -> Option<Self::Item>;
}
// 一个总是生成 i32 的自定义迭代器 —— 没得选
struct Counter { max: i32, current: i32 }
impl Iterator for Counter {
type Item = i32; // 每个实现恰好一个 Item 类型
fn next(&mut self) -> Option<i32> {
if self.current < self.max {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
// --- GENERIC 参数:每个类型多个实现 ---
trait Convert<T> {
fn convert(&self) -> T;
}
// 单个类型可以为 MANY 目标类型实现 Convert:
impl Convert<f64> for i32 {
fn convert(&self) -> f64 { *self as f64 }
}
impl Convert<String> for i32 {
fn convert(&self) -> String { self.to_string() }
}
}
何时使用哪个:
| 使用 | 何时 |
|---|---|
| 关联类型 | 每个实现类型恰好有一个自然输出/结果。Iterator::Item、Deref::Target、Add::Output |
| Generic 参数 | 类型可以为许多不同类型有意义地实现 trait。From<T>、AsRef<T>、PartialEq<Rhs> |
直觉:如果问“这个迭代器的 Item 是什么?“有意义,使用关联类型。如果问“这个可以转换成 f64 吗?转换成 String 吗?转换成 bool 吗?“有意义,使用 generic 参数。
#![allow(unused)]
fn main() {
// 现实世界示例:std::ops::Add
trait Add<Rhs = Self> {
type Output; // 关联类型 —— 加法有一个结果类型
fn add(self, rhs: Rhs) -> Self::Output;
}
// Rhs 是 generic 参数 —— 你可以加不同类型到 Meters:
struct Meters(f64);
struct Centimeters(f64);
impl Add<Meters> for Meters {
type Output = Meters;
fn add(self, rhs: Meters) -> Meters { Meters(self.0 + rhs.0) }
}
impl Add<Centimeters> for Meters {
type Output = Meters;
fn add(self, rhs: Centimeters) -> Meters { Meters(self.0 + rhs.0 / 100.0) }
}
}
Generic 关联类型(GATs)
自 Rust 1.65 起,关联类型可以有自己的 generic 参数。 这使得 lending 迭代器 成为可能 —— 返回绑定到迭代器而非底层集合的引用的迭代器:
#![allow(unused)]
fn main() {
// 无 GATs —— 无法表达 lending 迭代器:
// trait LendingIterator {
// type Item<'a>; // ← 这在 1.65 之前被拒绝
// }
// 有 GATs(Rust 1.65+):
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>>;
}
// 示例:生成重叠窗口的迭代器
struct WindowIter<'data> {
data: &'data [u8],
pos: usize,
window_size: usize,
}
impl<'data> LendingIterator for WindowIter<'data> {
type Item<'a> = &'a [u8] where Self: 'a;
fn next(&mut self) -> Option<&[u8]> {
if self.pos + self.window_size <= self.data.len() {
let window = &self.data[self.pos..self.pos + self.window_size];
self.pos += 1;
Some(window)
} else {
None
}
}
}
}
何时需要 GATs:Lending 迭代器、流式解析器,或任何关联类型的生命周期依赖于
&self借用的 trait。 对大多数代码,普通关联类型就足够了。
Supertraits 和 Trait 层次结构
Traits 可以要求其他 traits 作为先决条件,形成层次结构:
graph BT
Display["Display"]
Debug["Debug"]
Error["Error"]
Clone["Clone"]
Copy["Copy"]
PartialEq["PartialEq"]
Eq["Eq"]
PartialOrd["PartialOrd"]
Ord["Ord"]
Error --> Display
Error --> Debug
Copy --> Clone
Eq --> PartialEq
Ord --> Eq
Ord --> PartialOrd
PartialOrd --> PartialEq
style Display fill:#e8f4f8,stroke:#2980b9,color:#000
style Debug fill:#e8f4f8,stroke:#2980b9,color:#000
style Error fill:#fdebd0,stroke:#e67e22,color:#000
style Clone fill:#d4efdf,stroke:#27ae60,color:#000
style Copy fill:#d4efdf,stroke:#27ae60,color:#000
style PartialEq fill:#fef9e7,stroke:#f1c40f,color:#000
style Eq fill:#fef9e7,stroke:#f1c40f,color:#000
style PartialOrd fill:#fef9e7,stroke:#f1c40f,color:#000
style Ord fill:#fef9e7,stroke:#f1c40f,color:#000
箭头从 subtrait 指向 supertrait:实现
Error需要Display+Debug。
一个 trait 可以要求实现者也实现其他 traits:
#![allow(unused)]
fn main() {
use std::fmt;
// Display 是 Error 的 supertrait
trait Error: fmt::Display + fmt::Debug {
fn source(&self) -> Option<&(dyn Error + 'static)> { None }
}
// 任何实现 Error 的类型也必须实现 Display 和 Debug
// 构建你自己的层次结构:
trait Identifiable {
fn id(&self) -> u64;
}
trait Timestamped {
fn created_at(&self) -> chrono::DateTime<chrono::Utc>;
}
// Entity 要求两者:
trait Entity: Identifiable + Timestamped {
fn is_active(&self) -> bool;
}
// 实现 Entity 强制你实现所有三个:
struct User { id: u64, name: String, created: chrono::DateTime<chrono::Utc> }
impl Identifiable for User {
fn id(&self) -> u64 { self.id }
}
impl Timestamped for User {
fn created_at(&self) -> chrono::DateTime<chrono::Utc> { self.created }
}
impl Entity for User {
fn is_active(&self) -> bool { true }
}
}
Blanket 实现
为所有满足某些 bound 的类型实现 trait:
#![allow(unused)]
fn main() {
// std 这样做:任何实现 Display 的类型自动获得 ToString
impl<T: fmt::Display> ToString for T {
fn to_string(&self) -> String {
format!("{self}")
}
}
// 现在 i32、&str、你的自定义类型 —— 任何有 Display 的 —— 免费获得 to_string()。
// 你自己的 blanket 实现:
trait Loggable {
fn log(&self);
}
// 每个 Debug 类型自动成为 Loggable:
impl<T: std::fmt::Debug> Loggable for T {
fn log(&self) {
eprintln!("[LOG] {self:?}");
}
}
// 现在任何 Debug 类型都有 .log():
// 42.log(); // [LOG] 42
// "hello".log(); // [LOG] "hello"
// vec![1, 2, 3].log(); // [LOG] [1, 2, 3]
}
注意:Blanket impls 强大但不可逆 —— 你不能为已被 blanket impl 覆盖的类型添加 更具体的实现(orphan rules + coherence)。仔细设计它们。
Marker Traits
没有方法的 traits —— 它们标记类型具有某些属性:
#![allow(unused)]
fn main() {
// 标准库 marker traits:
// Send —— 安全在线程间转移
// Sync —— 安全在线程间共享 (&T)
// Unpin —— pinning 后安全移动
// Sized —— 编译时有已知大小
// Copy —— 可以用 memcpy 复制
// 你自己的 marker trait:
/// Marker:这个传感器已工厂校准
trait Calibrated {}
struct RawSensor { reading: f64 }
struct CalibratedSensor { reading: f64 }
impl Calibrated for CalibratedSensor {}
// 只有校准的传感器可以在生产中使用:
fn record_measurement<S: Calibrated>(sensor: &S) {
// ...
}
// record_measurement(&RawSensor { reading: 0.0 }); // ❌ 编译错误
// record_measurement(&CalibratedSensor { reading: 0.0 }); // ✅
}
这直接连接到第 3 章的type-state 模式。
Trait Object 安全规则
不是每个 trait 都可以用作 dyn Trait。一个 trait 仅当以下条件满足时才是object-safe的:
- trait 本身没有
Self: Sizedbound - 方法没有 generic 类型参数
- 没有在返回位置使用
Self(除非通过Box<Self>等间接方式) - 没有关联函数(方法必须有
&self、&mut self或self)
#![allow(unused)]
fn main() {
// ✅ Object-safe —— 可以用作 dyn Drawable
trait Drawable {
fn draw(&self);
fn bounding_box(&self) -> (f64, f64, f64, f64);
}
let shapes: Vec<Box<dyn Drawable>> = vec![/* ... */]; // ✅ 工作
// ❌ 不 object-safe —— 在返回位置使用 Self
trait Cloneable {
fn clone_self(&self) -> Self;
// ^^^^ 运行时无法知道具体大小
}
// let items: Vec<Box<dyn Cloneable>> = ...; // ❌ 编译错误
// ❌ 不 object-safe —— generic 方法
trait Converter {
fn convert<T>(&self) -> T;
// ^^^ vtable 无法包含无限单态化
}
// ❌ 不 object-safe —— 关联函数(无 self)
trait Factory {
fn create() -> Self;
// 无 &self —— 如何通过 trait object 调用这个?
}
}
变通方法:
#![allow(unused)]
fn main() {
// 添加 `where Self: Sized` 将方法从 vtable 排除:
trait MyTrait {
fn regular_method(&self); // 包含在 vtable 中
fn generic_method<T>(&self) -> T
where
Self: Sized; // 从 vtable 排除 —— 不能通过 dyn MyTrait 调用
}
// 现在 dyn MyTrait 有效,但 generic_method 只能
// 在具体类型已知时调用。
}
经验法则:如果计划使用
dyn Trait,保持方法简单 —— 无 generics,无返回类型中的Self,无Sizedbounds。如有疑问, 尝试let _: Box<dyn YourTrait>;让编译器告诉你。
Trait Objects 底层 —— vtables 和 Fat Pointers
&dyn Trait(或 Box<dyn Trait>)是fat pointer —— 两个机器字:
┌──────────────────────────────────────────────────┐
│ &dyn Drawable(64 位:总共 16 字节) │
├──────────────┬───────────────────────────────────┤
│ data_ptr │ vtable_ptr │
│ (8 字节) │ (8 字节) │
│ ↓ │ ↓ │
│ ┌─────────┐ │ ┌──────────────────────────────┐ │
│ │ Circle │ │ │ vtable for <Circle as │ │
│ │ { │ │ │ Drawable> │ │
│ │ r: 5.0 │ │ │ │ │
│ │ } │ │ │ drop_in_place: 0x7f...a0 │ │
│ └─────────┘ │ │ size: 8 │ │
│ │ │ align: 8 │ │
│ │ │ draw: 0x7f...b4 │ │
│ │ │ bounding_box: 0x7f...c8 │ │
│ │ └──────────────────────────────┘ │
└──────────────┴───────────────────────────────────┘
vtable 调用如何工作(例如 shape.draw()):
- 从 fat pointer 加载
vtable_ptr(第二个字) - 索引到 vtable 找到
draw函数指针 - 调用它,传递
data_ptr作为self参数
这与 C++ 虚函数调用的成本相似(每次调用一次指针间接),但 Rust 将 vtable 指针存储在 fat pointer 中而非对象内部 —— 所以栈上的普通 Circle 根本不携带 vtable 指针。
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
impl Drawable for Circle {
fn draw(&self) { println!("Drawing circle r={}", self.radius); }
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
struct Square { side: f64 }
impl Drawable for Square {
fn draw(&self) { println!("Drawing square s={}", self.side); }
fn area(&self) -> f64 { self.side * self.side }
}
fn main() {
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 3.0 }),
];
// 每个元素是 fat pointer:(data_ptr, vtable_ptr)
// Circle 和 Square 的 vtable 不同
for shape in &shapes {
shape.draw(); // vtable dispatch → Circle::draw 或 Square::draw
println!(" area = {:.2}", shape.area());
}
// 大小比较:
println!("size_of::<&Circle>() = {}", size_of::<&Circle>());
// → 8 字节(一个指针 —— 编译器知道类型)
println!("size_of::<&dyn Drawable>() = {}", size_of::<&dyn Drawable>());
// → 16 字节(data_ptr + vtable_ptr)
}
性能成本模型:
| 方面 | 静态 dispatch(impl Trait / generics) | 动态 dispatch(dyn Trait) |
|---|---|---|
| 调用开销 | 零 —— LLVM 内联 | 每次调用一次指针间接 |
| 内联 | ✅ 编译器可以内联 | ❌ 不透明函数指针 |
| 二进制大小 | 更大(每个类型一份副本) | 更小(共享代码) |
| 指针大小 | 瘦(1 字) | 胖(2 字) |
| 异构集合 | ❌ | ✅ Vec<Box<dyn Trait>> |
vtable 成本何时重要:在调用 trait 方法数百万次的紧密循环中, 间接和无法内联可能显著(2-10 倍慢)。对于冷路径、配置或插件架构,
dyn Trait的灵活性值得这个小成本。
Higher-Ranked Trait Bounds(HRTBs)
有时需要一个函数可以使用任何生命周期的引用工作,而不是一个特定的生命周期。这就是 for<'a> 语法出现的地方:
// 问题:这个函数需要一个闭包,可以处理
// 任何生命周期的引用,而不仅是一个特定生命周期。
// ❌ 这太局限了 —— 'a 由调用者固定:
// fn apply<'a, F: Fn(&'a str) -> &'a str>(f: F, data: &'a str) -> &'a str
// ✅ HRTB:F 必须对所有可能的生命周期有效:
fn apply<F>(f: F, data: &str) -> &str
where
F: for<'a> Fn(&'a str) -> &'a str,
{
f(data)
}
fn main() {
let result = apply(|s| s.trim(), " hello ");
println!("{result}"); // "hello"
}
何时遇到 HRTBs:
Fn(&T) -> &Utraits —— 编译器在大多数情况下自动推断for<'a>- 必须跨不同借用工作的自定义 trait 实现
- 带
serde的反序列化:for<'de> Deserialize<'de>
// serde 的 DeserializeOwned 定义为:
// trait DeserializeOwned: for<'de> Deserialize<'de> {}
// 含义:"可以从任何生命周期的数据反序列化"
// (即结果不从输入借用)
use serde::de::DeserializeOwned;
fn parse_json<T: DeserializeOwned>(input: &str) -> T {
serde_json::from_str(input).unwrap()
}
实用建议:你很少会自己写
for<'a>。它主要出现在 闭包参数的 trait bounds 中,编译器隐式处理它。 但在错误消息中识别它(“expected afor<'a> Fn(&'a ...)bound”) 帮助你理解编译器要求什么。
impl Trait —— 参数位置 vs 返回位置
impl Trait 出现在两个位置,具有不同的语义:
#![allow(unused)]
fn main() {
// --- 参数位置 impl Trait(APIT) ---
// "调用者选择类型" —— generic 参数的语法糖
fn print_all(items: impl Iterator<Item = i32>) {
for item in items { println!("{item}"); }
}
// 等价于:
fn print_all_verbose<I: Iterator<Item = i32>>(items: I) {
for item in items { println!("{item}"); }
}
// 调用者决定:print_all(vec![1,2,3].into_iter())
// print_all(0..10)
// --- 返回位置 impl Trait(RPIT) ---
// "被调用者选择类型" —— 函数挑选一个具体类型
fn evens(limit: i32) -> impl Iterator<Item = i32> {
(0..limit).filter(|x| x % 2 == 0)
// 具体类型是 Filter<Range<i32>, Closure>
// 但调用者只看到"某个 Iterator<Item = i32>"
}
}
关键区别:
APIT(fn foo(x: impl T)) | RPIT(fn foo() -> impl T) | |
|---|---|---|
| 谁选择类型? | 调用者 | 被调用者(函数体) |
| 单态化? | 是 —— 每个类型一份副本 | 是 —— 一个具体类型 |
| Turbofish? | 否(foo::<X>() 不允许) | N/A |
| 等价于 | fn foo<X: T>(x: X) | 存在类型 |
Trait 定义中的 RPIT(RPITIT)
自 Rust 1.75 起,你可以在 trait 定义中直接使用 -> impl Trait:
#![allow(unused)]
fn main() {
trait Container {
fn items(&self) -> impl Iterator<Item = &str>;
// ^^^^ 每个实现者返回自己的具体类型
}
struct CsvRow {
fields: Vec<String>,
}
impl Container for CsvRow {
fn items(&self) -> impl Iterator<Item = &str> {
self.fields.iter().map(String::as_str)
}
}
struct FixedFields;
impl Container for FixedFields {
fn items(&self) -> impl Iterator<Item = &str> {
["host", "port", "timeout"].into_iter()
}
}
}
Rust 1.75 之前,你必须使用
Box<dyn Iterator>或关联类型 来在 traits 中实现这个。RPITIT 消除了分配。
impl Trait vs dyn Trait —— 决策指南
编译时知道具体类型?
├── 是 → 使用 impl Trait 或 generics(零成本,可内联)
└── 否 → 需要异构集合?
├── 是 → 使用 dyn Trait(Box<dyn T>、&dyn T)
└── 否 → 需要跨 API 边界的相同 trait object?
├── 是 → 使用 dyn Trait
└── 否 → 使用 generics / impl Trait
| 特性 | impl Trait | dyn Trait |
|---|---|---|
| Dispatch | 静态(单态化) | 动态(vtable) |
| 性能 | 最佳 —— 可内联 | 每次调用一次间接 |
| 异构集合 | ❌ | ✅ |
| 二进制大小每个类型 | 每个类型一份副本 | 共享代码 |
| Trait 必须 object-safe? | 否 | 是 |
| 在 trait 定义中工作 | ✅(Rust 1.75+) | 总是 |
带 Any 和 TypeId 的类型擦除
有时你需要存储未知类型的值并在之后向下转换 —— 这种模式在 C 的 void* 或 C# 的 object 中很熟悉。Rust 通过 std::any::Any 提供这个:
use std::any::Any;
// 存储异构值:
fn log_value(value: &dyn Any) {
if let Some(s) = value.downcast_ref::<String>() {
println!("String: {s}");
} else if let Some(n) = value.downcast_ref::<i32>() {
println!("i32: {n}");
} else {
// TypeId 允许你在运行时检查类型:
println!("Unknown type: {:?}", value.type_id());
}
}
// 对插件系统、事件总线或 ECS 风格架构有用:
struct AnyMap(std::collections::HashMap<std::any::TypeId, Box<dyn Any + Send>>);
impl AnyMap {
fn new() -> Self { AnyMap(std::collections::HashMap::new()) }
fn insert<T: Any + Send + 'static>(&mut self, value: T) {
self.0.insert(std::any::TypeId::of::<T>(), Box::new(value));
}
fn get<T: Any + Send + 'static>(&self) -> Option<&T> {
self.0.get(&std::any::TypeId::of::<T>())?
.downcast_ref()
}
}
fn main() {
let mut map = AnyMap::new();
map.insert(42_i32);
map.insert(String::from("hello"));
assert_eq!(map.get::<i32>(), Some(&42));
assert_eq!(map.get::<String>().map(|s| s.as_str()), Some("hello"));
assert_eq!(map.get::<f64>(), None); // 从未插入
}
何时使用
Any:插件/扩展系统、类型索引映射(typemap)、 错误向下转换(anyhow::Error::downcast_ref)。当类型集在编译时已知时, 优先 generics 或 trait objects ——Any是最后手段, 用编译时安全换取灵活性。
扩展 Traits —— 为你不拥有的类型添加方法
Rust 的 orphan rule 阻止你为外部类型实现外部 trait。 扩展 traits 是标准的变通方法:在你的 crate 中定义一个新 trait,其方法 对任何满足 bound 的类型都有 blanket 实现。调用者导入 trait, 新方法出现在现有类型上。
这种模式在 Rust 生态系统中无处不在:itertools::Itertools、futures::StreamExt、
tokio::io::AsyncReadExt、tower::ServiceExt。
问题
#![allow(unused)]
fn main() {
// 我们想为所有生成 f64 的迭代器添加 .mean() 方法。
// 但 Iterator 在 std 中定义,f64 是原始类型 —— orphan rule 阻止:
//
// impl<I: Iterator<Item = f64>> I { // ❌ 不能为外部类型添加固有方法
// fn mean(self) -> f64 { ... }
// }
}
解决方案:扩展 Trait
#![allow(unused)]
fn main() {
/// 迭代器数值方法的扩展 trait。
pub trait IteratorExt: Iterator {
/// 计算算术平均值。为空迭代器返回 `None`。
fn mean(self) -> Option<f64>
where
Self: Sized,
Self::Item: Into<f64>;
}
// Blanket 实现 —— 自动应用于所有迭代器
impl<I: Iterator> IteratorExt for I {
fn mean(self) -> Option<f64>
where
Self: Sized,
Self::Item: Into<f64>,
{
let mut sum: f64 = 0.0;
let mut count: u64 = 0;
for item in self {
sum += item.into();
count += 1;
}
if count == 0 { None } else { Some(sum / count as f64) }
}
}
// 用法 —— 只需导入 trait:
use crate::IteratorExt; // 一个导入,方法出现在所有迭代器上
fn analyze_temperatures(readings: &[f64]) -> Option<f64> {
readings.iter().copied().mean() // .mean() 现在可用!
}
fn analyze_sensor_data(data: &[i32]) -> Option<f64> {
data.iter().copied().mean() // 也适用于 i32(i32: Into<f64>)
}
}
现实世界示例:诊断 Result 扩展
#![allow(unused)]
fn main() {
use std::collections::HashMap;
struct DiagResult {
component: String,
passed: bool,
message: String,
}
/// Vec<DiagResult> 的扩展 trait —— 添加领域特定分析方法。
pub trait DiagResultsExt {
fn passed_count(&self) -> usize;
fn failed_count(&self) -> usize;
fn overall_pass(&self) -> bool;
fn failures_by_component(&self) -> HashMap<String, Vec<&DiagResult>>;
}
impl DiagResultsExt for Vec<DiagResult> {
fn passed_count(&self) -> usize {
self.iter().filter(|r| r.passed).count()
}
fn failed_count(&self) -> usize {
self.iter().filter(|r| !r.passed).count()
}
fn overall_pass(&self) -> bool {
self.iter().all(|r| r.passed)
}
fn failures_by_component(&self) -> HashMap<String, Vec<&DiagResult>> {
let mut map = HashMap::new();
for r in self.iter().filter(|r| !r.passed) {
map.entry(r.component.clone()).or_default().push(r);
}
map
}
}
// 现在任何 Vec<DiagResult> 都有这些方法:
fn report(results: Vec<DiagResult>) {
if !results.overall_pass() {
let failures = results.failures_by_component();
for (component, fails) in &failures {
eprintln!("{component}: {} failures", fails.len());
}
}
}
}
命名约定
Rust 生态系统使用一致的 Ext 后缀:
| Crate | 扩展 Trait | 扩展 |
|---|---|---|
itertools | Itertools | Iterator |
futures | StreamExt、FutureExt | Stream、Future |
tokio | AsyncReadExt、AsyncWriteExt | AsyncRead、AsyncWrite |
tower | ServiceExt | Service |
bytes | BufMut(部分) | &mut [u8] |
| 你的 crate | DiagResultsExt | Vec<DiagResult> |
何时使用
| 情况 | 使用扩展 Trait? |
|---|---|
| 为外部类型添加便利方法 | ✅ |
| 在 generic 集合上分组领域特定逻辑 | ✅ |
| 方法需要访问私有字段 | ❌(使用 wrapper/newtype) |
| 方法逻辑上属于你控制的新类型 | ❌(直接添加到你的类型) |
| 你希望方法无需任何导入就可用 | ❌(仅固有方法) |
Enum Dispatch —— 无 dyn 的静态多态
当你有实现 trait 的封闭集类型时,你可以用包含具体类型的 enum 替换 dyn Trait。这消除了 vtable 间接和堆分配,同时保留相同的调用者面向接口。
dyn Trait 的问题
#![allow(unused)]
fn main() {
trait Sensor {
fn read(&self) -> f64;
fn name(&self) -> &str;
}
struct Gps { lat: f64, lon: f64 }
struct Thermometer { temp_c: f64 }
struct Accelerometer { g_force: f64 }
impl Sensor for Gps {
fn read(&self) -> f64 { self.lat }
fn name(&self) -> &str { "GPS" }
}
impl Sensor for Thermometer {
fn read(&self) -> f64 { self.temp_c }
fn name(&self) -> &str { "Thermometer" }
}
impl Sensor for Accelerometer {
fn read(&self) -> f64 { self.g_force }
fn name(&self) -> &str { "Accelerometer" }
}
// 带 dyn 的异构集合 —— 工作,但有成本:
fn read_all_dyn(sensors: &[Box<dyn Sensor>]) -> Vec<f64> {
sensors.iter().map(|s| s.read()).collect()
// 每个 .read() 通过 vtable 间接
// 每个 Box 在堆上分配
}
}
Enum Dispatch 解决方案
// 用 enum 替换 trait object:
enum AnySensor {
Gps(Gps),
Thermometer(Thermometer),
Accelerometer(Accelerometer),
}
impl AnySensor {
fn read(&self) -> f64 {
match self {
AnySensor::Gps(s) => s.read(),
AnySensor::Thermometer(s) => s.read(),
AnySensor::Accelerometer(s) => s.read(),
}
}
fn name(&self) -> &str {
match self {
AnySensor::Gps(s) => s.name(),
AnySensor::Thermometer(s) => s.name(),
AnySensor::Accelerometer(s) => s.name(),
}
}
}
// 现在:无堆分配,无 vtable,内联存储
fn read_all(sensors: &[AnySensor]) -> Vec<f64> {
sensors.iter().map(|s| s.read()).collect()
// 每个 .read() 是 match 分支 —— 编译器可以内联所有
}
fn main() {
let sensors = vec![
AnySensor::Gps(Gps { lat: 47.6, lon: -122.3 }),
AnySensor::Thermometer(Thermometer { temp_c: 72.5 }),
AnySensor::Accelerometer(Accelerometer { g_force: 1.02 }),
];
for sensor in &sensors {
println!("{}: {:.2}", sensor.name(), sensor.read());
}
}
为 Enum 实现 Trait
为了互操作性,你可以在 enum 本身上实现原始 trait:
#![allow(unused)]
fn main() {
impl Sensor for AnySensor {
fn read(&self) -> f64 {
match self {
AnySensor::Gps(s) => s.read(),
AnySensor::Thermometer(s) => s.read(),
AnySensor::Accelerometer(s) => s.read(),
}
}
fn name(&self) -> &str {
match self {
AnySensor::Gps(s) => s.name(),
AnySensor::Thermometer(s) => s.name(),
AnySensor::Accelerometer(s) => s.name(),
}
}
}
// 现在 AnySensor 可以通过 generics 在任何期望 Sensor 的地方工作:
fn report<S: Sensor>(s: &S) {
println!("{}: {:.2}", s.name(), s.read());
}
}
用宏减少样板代码
match 分支委托是重复的。宏消除了它:
#![allow(unused)]
fn main() {
macro_rules! dispatch_sensor {
($self:expr, $method:ident $(, $arg:expr)*) => {
match $self {
AnySensor::Gps(s) => s.$method($($arg),*),
AnySensor::Thermometer(s) => s.$method($($arg),*),
AnySensor::Accelerometer(s) => s.$method($($arg),*),
}
};
}
impl Sensor for AnySensor {
fn read(&self) -> f64 { dispatch_sensor!(self, read) }
fn name(&self) -> &str { dispatch_sensor!(self, name) }
}
}
对于更大的项目,enum_dispatch crate 完全自动化这个:
#![allow(unused)]
fn main() {
use enum_dispatch::enum_dispatch;
#[enum_dispatch]
trait Sensor {
fn read(&self) -> f64;
fn name(&self) -> &str;
}
#[enum_dispatch(Sensor)]
enum AnySensor {
Gps,
Thermometer,
Accelerometer,
}
// 所有委托代码自动生成。
}
dyn Trait vs Enum Dispatch —— 决策指南
类型集封闭(编译时已知)?
├── 是 → 优先 enum dispatch(更快,无堆分配)
│ ├── 少数变体(< ~20)? → 手动 enum
│ └── 许多变体或增长? → enum_dispatch crate
└── 否 → 必须使用 dyn Trait(插件、用户提供的类型)
| 属性 | dyn Trait | Enum Dispatch |
|---|---|---|
| Dispatch 成本 | Vtable 间接(~2ns) | 分支预测(~0.3ns) |
| 堆分配 | 通常(Box) | 无(内联) |
| 缓存友好 | 否(指针追逐) | 是(连续) |
| 对新类型开放 | ✅(任何人都可以实现) | ❌(封闭集) |
| 代码大小 | 共享 | 每个变体一份副本 |
| Trait 必须 object-safe | 是 | 否 |
| 添加变体 | 无代码更改 | 更新 enum + match 分支 |
何时使用 Enum Dispatch
| 场景 | 推荐 |
|---|---|
| 诊断测试类型(CPU、GPU、NIC、Memory、…) | ✅ Enum dispatch —— 封闭集,编译时已知 |
| 总线协议(SPI、I2C、UART、…) | ✅ Enum dispatch 或 Config trait |
| 插件系统(用户运行时加载 .so) | ❌ 使用 dyn Trait |
| 2-3 个变体 | ✅ 手动 enum dispatch |
| 10+ 变体带许多方法 | ✅ enum_dispatch crate |
| 性能关键的内部循环 | ✅ Enum dispatch(消除 vtable) |
Capability Mixins —— 组合式诊断功能
在 Ruby 中,Mixin 是模块化的基石 —— 你可以 include 一个模块来获得它的方法,同时保持继承层次结构的扁平。在 Rust 中,你可以使用 trait 的组合和 blanket impls 实现类似的效果,但要灵活得多:方法可以有条件地存在(基于 associated types),组合在编译时验证,没有运行时成本。
问题:跨诊断共享外设访问
固件诊断代码通常需要从多个源读取:
#![allow(unused)]
fn main() {
// ❌ 现实世界中 C/C++ 风格的诊断函数
fn check_thermal_thresholds() -> bool {
// 从 SPI 读取 thermocouple
let tc = spi_read_thermocouple();
// 从 I2C 读取风扇转速
let fan_rpm = i2c_read_fan(0);
// 从 IPMI/BMC 读取温度传感器
let bmc_temp = ipmi_get_sensor(0x20);
// 从 GPIO 控制风扇使能
gpio_set_fan_enable(true);
tc < 85.0 && fan_rpm > 4000 && bmc_temp < 90.0
}
}
这个函数“知道“太多东西了 —— 它直接调用 spi_read_thermocouple()、i2c_read_fan() 等。测试它需要:
- 模拟这些全局函数(在 C 中很难)
- 或者在实际硬件上运行(在 CI 中不可能)
解决方案:Capability Mixins
这个模式将能力(“我有 SPI”、“我有 I2C”)与功能(“我可以读取 thermocouple”、“我可以检查风扇健康”)分开。
步骤 1 —— 定义“能力“ Traits(Ingredient Traits)
每个能力 trait 暴露一个 associated type 和一个 accessor 方法:
#![allow(unused)]
fn main() {
/// 能力:有一个 SPI 总线
pub trait HasSpi {
type Spi: SpiBus;
fn spi(&self) -> &Self::Spi;
}
/// 能力:有一个 I2C 总线
pub trait HasI2c {
type I2c: I2cBus;
fn i2c(&self) -> &Self::I2c;
}
/// 能力:有一个 GPIO 引脚
pub trait HasGpio {
type Gpio: GpioPin;
fn gpio(&self) -> &Self::Gpio;
}
/// 能力:有一个 IPMI BMC 连接
pub trait HasIpmi {
type Ipmi: IpmiBmc;
fn ipmi(&self) -> &Self::Ipmi;
}
}
每个 ingredient 都很小、generic,可以独立测试。
步骤 2 —— 定义“Mixin“ Traits
Mixin trait 声明其所需的 ingredient 作为 supertraits,然后通过默认方法提供所有方法 —— 实现者自动获得它们:
#![allow(unused)]
fn main() {
/// Mixin:风扇诊断 —— 需要 I2C(转速计)+ GPIO(PWM 使能)
pub trait FanDiagMixin: HasI2c + HasGpio {
/// 从 I2C 转速计读取风扇 RPM。
fn read_fan_rpm(&self, fan_id: u8) -> io::Result<u32> {
let mut buf = [0u8; 2];
self.i2c().i2c_read(0x48 + fan_id, 0x00, &mut buf)?;
Ok(u16::from_be_bytes(buf) as u32 * 60) // 转速计数 → RPM
}
/// 启用或禁用风扇 PWM 输出通过 GPIO。
fn set_fan_pwm(&self, enable: bool) -> io::Result<()> {
if enable { self.gpio().set_high() }
else { self.gpio().set_low() }
}
/// 完整风扇健康检查 —— 读取 RPM + 验证在阈值内。
fn check_fan_health(&self, fan_id: u8, min_rpm: u32) -> io::Result<bool> {
let rpm = self.read_fan_rpm(fan_id)?;
Ok(rpm >= min_rpm)
}
}
/// Mixin:温度监控 —— 需要 SPI(热电偶 ADC)+ IPMI(BMC 传感器)
pub trait TempMonitorMixin: HasSpi + HasIpmi {
/// 通过 SPI ADC 读取热电偶(例如 MAX31855)。
fn read_thermocouple(&self) -> io::Result<f64> {
let mut rx = [0u8; 4];
self.spi().spi_transfer(&[0x00; 4], &mut rx)?;
let raw = i32::from_be_bytes(rx) >> 18; // 14-bit 有符号
Ok(raw as f64 * 0.25)
}
/// 读取 BMC 管理的温度传感器通过 IPMI。
fn read_bmc_temp(&self, sensor_id: u8) -> io::Result<f64> {
self.ipmi().read_sensor(sensor_id)
}
/// 交叉验证:热电偶 vs BMC 必须在 delta 内一致。
fn validate_temps(&self, sensor_id: u8, max_delta: f64) -> io::Result<bool> {
let tc = self.read_thermocouple()?;
let bmc = self.read_bmc_temp(sensor_id)?;
Ok((tc - bmc).abs() <= max_delta)
}
}
/// Mixin:电源排序 —— 需要 GPIO(rail 使能)+ IPMI(事件日志)
pub trait PowerSeqMixin: HasGpio + HasIpmi {
/// 断言 power-good GPIO 并通过 IPMI 传感器验证。
fn enable_power_rail(&self, sensor_id: u8) -> io::Result<bool> {
self.gpio().set_high()?;
std::thread::sleep(std::time::Duration::from_millis(50));
let voltage = self.ipmi().read_sensor(sensor_id)?;
Ok(voltage > 0.8) // 高于 80% 标称值 = 良好
}
/// 取消断言电源并通过 IPMI OEM 命令记录关闭。
fn disable_power_rail(&self) -> io::Result<()> {
self.gpio().set_low()?;
// 记录 OEM "power rail disabled" 事件到 BMC
self.ipmi().raw_command(0x2E, 0x01, &[0x00, 0x01])?;
Ok(())
}
}
}
步骤 3 —— Blanket Impls 使其真正成为“Mixin“
神奇的一行 —— 提供 ingredient,获得方法:
#![allow(unused)]
fn main() {
impl<T: HasI2c + HasGpio> FanDiagMixin for T {}
impl<T: HasSpi + HasIpmi> TempMonitorMixin for T {}
impl<T: HasGpio + HasIpmi> PowerSeqMixin for T {}
}
任何实现正确 ingredient traits 的结构体自动获得每个 mixin 方法 —— 无样板代码,无转发,无继承。
步骤 4 —— 生产环境接线
#![allow(unused)]
fn main() {
// ── 具体总线实现(Linux 平台)────────────────
struct LinuxSpi { dev: String }
struct LinuxI2c { dev: String }
struct SysfsGpio { pin: u32 }
struct IpmiTool { timeout_secs: u32 }
impl SpiBus for LinuxSpi {
fn spi_transfer(&self, _tx: &[u8], _rx: &mut [u8]) -> io::Result<()> {
// spidev ioctl —— 省略细节
Ok(())
}
}
impl I2cBus for LinuxI2c {
fn i2c_read(&self, _addr: u8, _reg: u8, _buf: &mut [u8]) -> io::Result<()> {
// i2c-dev ioctl —— 省略细节
Ok(())
}
fn i2c_write(&self, _addr: u8, _reg: u8, _data: &[u8]) -> io::Result<()> { Ok(()) }
}
impl GpioPin for SysfsGpio {
fn set_high(&self) -> io::Result<()> { /* /sys/class/gpio */ Ok(()) }
fn set_low(&self) -> io::Result<()> { Ok(()) }
fn read_level(&self) -> io::Result<bool> { Ok(true) }
}
impl IpmiBmc for IpmiTool {
fn raw_command(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result<Vec<u8>> {
// 调用 ipmitool —— 省略细节
Ok(vec![])
}
fn read_sensor(&self, _id: u8) -> io::Result<f64> { Ok(25.0) }
}
// ── 生产平台 —— 全部四个总线 ────────────────
struct DiagPlatform {
spi: LinuxSpi,
i2c: LinuxI2c,
gpio: SysfsGpio,
ipmi: IpmiTool,
}
impl HasSpi for DiagPlatform { type Spi = LinuxSpi; fn spi(&self) -> &LinuxSpi { &self.spi } }
impl HasI2c for DiagPlatform { type I2c = LinuxI2c; fn i2c(&self) -> &LinuxI2c { &self.i2c } }
impl HasGpio for DiagPlatform { type Gpio = SysfsGpio; fn gpio(&self) -> &SysfsGpio { &self.gpio } }
impl HasIpmi for DiagPlatform { type Ipmi = IpmiTool; fn ipmi(&self) -> &IpmiTool { &self.ipmi } }
// DiagPlatform 现在拥有 ALL mixin 方法:
fn production_diagnostics(platform: &DiagPlatform) -> io::Result<()> {
let rpm = platform.read_fan_rpm(0)?; // 来自 FanDiagMixin
let tc = platform.read_thermocouple()?; // 来自 TempMonitorMixin
let ok = platform.enable_power_rail(42)?; // 来自 PowerSeqMixin
println!("Fan: {rpm} RPM, Temp: {tc}°C, Power: {ok}");
Ok(())
}
}
步骤 5 —— 使用 Mock 测试(无需硬件)
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
struct MockSpi { temp: Cell<f64> }
struct MockI2c { rpm: Cell<u32> }
struct MockGpio { level: Cell<bool> }
struct MockIpmi { sensor_val: Cell<f64> }
impl SpiBus for MockSpi {
fn spi_transfer(&self, _tx: &[u8], rx: &mut [u8]) -> io::Result<()> {
// 编码 mock 温度为 MAX31855 格式
let raw = ((self.temp.get() / 0.25) as i32) << 18;
rx.copy_from_slice(&raw.to_be_bytes());
Ok(())
}
}
impl I2cBus for MockI2c {
fn i2c_read(&self, _addr: u8, _reg: u8, buf: &mut [u8]) -> io::Result<()> {
let tach = (self.rpm.get() / 60) as u16;
buf.copy_from_slice(&tach.to_be_bytes());
Ok(())
}
fn i2c_write(&self, _: u8, _: u8, _: &[u8]) -> io::Result<()> { Ok(()) }
}
impl GpioPin for MockGpio {
fn set_high(&self) -> io::Result<()> { self.level.set(true); Ok(()) }
fn set_low(&self) -> io::Result<()> { self.level.set(false); Ok(()) }
fn read_level(&self) -> io::Result<bool> { Ok(self.level.get()) }
}
impl IpmiBmc for MockIpmi {
fn raw_command(&self, _: u8, _: u8, _: &[u8]) -> io::Result<Vec<u8>> { Ok(vec![]) }
fn read_sensor(&self, _: u8) -> io::Result<f64> { Ok(self.sensor_val.get()) }
}
// ── 部分平台:仅风扇相关总线 ─────────────────
struct FanTestRig {
i2c: MockI2c,
gpio: MockGpio,
}
impl HasI2c for FanTestRig { type I2c = MockI2c; fn i2c(&self) -> &MockI2c { &self.i2c } }
impl HasGpio for FanTestRig { type Gpio = MockGpio; fn gpio(&self) -> &MockGpio { &self.gpio } }
// FanTestRig 获得 FanDiagMixin 但没有 TempMonitorMixin 或 PowerSeqMixin
#[test]
fn fan_health_check_passes_above_threshold() {
let rig = FanTestRig {
i2c: MockI2c { rpm: Cell::new(6000) },
gpio: MockGpio { level: Cell::new(false) },
};
assert!(rig.check_fan_health(0, 4000).unwrap());
}
#[test]
fn fan_health_check_fails_below_threshold() {
let rig = FanTestRig {
i2c: MockI2c { rpm: Cell::new(2000) },
gpio: MockGpio { level: Cell::new(false) },
};
assert!(!rig.check_fan_health(0, 4000).unwrap());
}
}
}
注意 FanTestRig 只实现了 HasI2c + HasGpio —— 它自动获得 FanDiagMixin,但编译器拒绝 rig.read_thermocouple(),因为 HasSpi 不满足。这是编译时强制的 mixin 作用域。
条件方法 —— 超越 Ruby 能做的
添加 where bounds 到单个默认方法。方法仅当 associated type 满足额外 bound 时才存在:
#![allow(unused)]
fn main() {
/// 支持 DMA 的 SPI 控制器的 marker trait
pub trait DmaCapable: SpiBus {
fn dma_transfer(&self, tx: &[u8], rx: &mut [u8]) -> io::Result<()>;
}
/// 支持中断的 GPIO 引脚的 marker trait
pub trait InterruptCapable: GpioPin {
fn wait_for_edge(&self, timeout_ms: u32) -> io::Result<bool>;
}
pub trait AdvancedDiagMixin: HasSpi + HasGpio {
// 总是可用
fn basic_probe(&self) -> io::Result<bool> {
let mut rx = [0u8; 1];
self.spi().spi_transfer(&[0xFF], &mut rx)?;
Ok(rx[0] != 0x00)
}
// 仅当 SPI 控制器支持 DMA 时存在
fn bulk_sensor_read(&self, buf: &mut [u8]) -> io::Result<()>
where
Self::Spi: DmaCapable,
{
self.spi().dma_transfer(&vec![0x00; buf.len()], buf)
}
// 仅当 GPIO 引脚支持中断时存在
fn wait_for_fault_signal(&self, timeout_ms: u32) -> io::Result<bool>
where
Self::Gpio: InterruptCapable,
{
self.gpio().wait_for_edge(timeout_ms)
}
}
impl<T: HasSpi + HasGpio> AdvancedDiagMixin for T {}
}
如果你的平台 SPI 不支持 DMA,调用 bulk_sensor_read() 是编译错误,不是运行时崩溃。Ruby 中最接近的等价物是 respond_to? 检查 —— 但它发生在部署时,不是编译时。
可组合性:叠加 Mixins
多个 mixins 可以共享相同的 ingredient —— 没有菱形问题:
┌─────────────┐ ┌───────────┐ ┌──────────────┐
│ FanDiagMixin│ │TempMonitor│ │ PowerSeqMixin│
│ (I2C+GPIO) │ │ (SPI+IPMI)│ │ (GPIO+IPMI) │
└──────┬──────┘ └─────┬─────┘ └──────┬───────┘
│ │ │
│ ┌─────────────┴─────────────┐ │
└──►│ DiagPlatform │◄──┘
│ HasSpi+HasI2c+HasGpio │
│ +HasIpmi │
└───────────────────────────┘
DiagPlatform 实现 HasGpio 一次,FanDiagMixin 和 PowerSeqMixin 都使用相同的 self.gpio()。在 Ruby 中,这将是两个模块都调用 self.gpio_pin —— 但如果它们期望不同的引脚编号,你将在运行时发现冲突。在 Rust 中,你可以在类型级别消除歧义。
对比:Ruby Mixins vs Rust Capability Mixins
| 维度 | Ruby Mixins | Rust Capability Mixins |
|---|---|---|
| Dispatch | 运行时(方法表查找) | 编译时(单态化) |
| 安全组合 | MRO 线性化隐藏冲突 | 编译器拒绝歧义 |
| 条件方法 | 运行时 respond_to? | 编译时 where bounds |
| 开销 | 方法 dispatch + GC | 零成本(内联) |
| 可测试性 | 通过元编程 stub/mock | generic 超过 mock 类型 |
| 添加新总线 | 运行时 include | 添加 ingredient trait,重新编译 |
| 运行时灵活性 | extend、prepend、开放类 | 无(完全静态) |
何时使用 Capability Mixins
| 场景 | 使用 Mixins? |
|---|---|
| 多个诊断共享总线读取逻辑 | ✅ |
| 测试 harness 需要不同的总线子集 | ✅(部分 ingredient 结构体) |
| 方法仅对某些总线能力有效(DMA、IRQ) | ✅(条件 where bounds) |
| 需要运行时模块加载(插件) | ❌(使用 dyn Trait 或 enum dispatch) |
| 单个结构体带一个总线 —— 不需要共享 | ❌(保持简单) |
| 带有相干性问题的 cross-crate ingredients | ⚠️(使用 newtype wrappers) |
关键要点 —— Capability Mixins
- Ingredient trait = associated type + accessor 方法(例如
HasSpi)- Mixin trait = ingredient 的 supertrait bounds + 默认方法体
- Blanket impl =
impl<T: HasX + HasY> Mixin for T {}—— 自动注入方法- 条件方法 = 单个默认方法上的
where Self::Spi: DmaCapable- 部分平台 = 仅实现所需 ingredient 的测试结构体
- 无运行时成本 —— 编译器为每个平台类型生成专用代码
Typed Commands —— GADT 风格的返回类型安全
在 Haskell 中,广义代数数据类型(GADTs) 让数据类型的每个构造函数细化类型参数 —— 所以 Expr Int 和 Expr Bool 被类型检查器强制执行。Rust 没有直接的 GADT 语法,但带 associated types 的 traits 实现相同的保证:命令类型决定响应类型,搞混它们是编译错误。
这个模式对于硬件诊断特别强大,其中 IPMI 命令、寄存器读取和传感器查询各自返回不同的物理量,绝不应该混淆。
问题:未类型的 Vec<u8> 沼泽
大多数 C/C++ IPMI 栈 —— 以及天真的 Rust 移植 —— 到处使用原始字节:
#![allow(unused)]
fn main() {
use std::io;
struct BmcConnectionUntyped { timeout_secs: u32 }
impl BmcConnectionUntyped {
fn raw_command(&self, net_fn: u8, cmd: u8, data: &[u8]) -> io::Result<Vec<u8>> {
// ... 调用 ipmitool ...
Ok(vec![0x00, 0x19, 0x00]) // 存根
}
}
fn diagnose_thermal_untyped(bmc: &BmcConnectionUntyped) -> io::Result<()> {
// 读取 CPU 温度 —— 传感器 ID 0x20
let raw = bmc.raw_command(0x04, 0x2D, &[0x20])?;
let cpu_temp = raw[0] as f64; // 🤞 希望字节 0 是读数
// 读取风扇转速 —— 传感器 ID 0x30
let raw = bmc.raw_command(0x04, 0x2D, &[0x30])?;
let fan_rpm = raw[0] as u32; // 🐛 BUG:风扇转速是 2 字节 LE
// 读取入口电压 —— 传感器 ID 0x40
let raw = bmc.raw_command(0x04, 0x2D, &[0x40])?;
let voltage = raw[0] as f64; // 🐛 BUG:需要除以 1000
// 🐛 比较 °C 和 RPM —— 编译通过,但无意义
if cpu_temp > fan_rpm as f64 {
println!("uh oh");
}
// 🐛 传递 Volts 作为温度 —— 编译完全没问题
log_temp_untyped(voltage);
log_volts_untyped(cpu_temp);
Ok(())
}
fn log_temp_untyped(t: f64) { println!("Temp: {t}°C"); }
fn log_volts_untyped(v: f64) { println!("Voltage: {v}V"); }
}
每个读数都是 f64 —— 编译器不知道哪一个是温度,哪一个是 RPM,哪一个是电压。四个不同的 bug 编译时没有警告:
| # | Bug | 后果 | 发现时间 |
|---|---|---|---|
| 1 | 风扇 RPM 解析为 1 字节而不是 2 | 读到 25 RPM 而不是 6400 | 生产环境,凌晨 3 点风扇故障告警 |
| 2 | 电压未除以 1000 | 12000V 而不是 12.0V | 阈值检查标记每个 PSU |
| 3 | 比较 °C 和 RPM | 无意义的布尔值 | 可能永远不会 |
| 4 | 电压传递给 log_temp_untyped() | 日志中的静默数据损坏 | 6 个月后,阅读历史记录 |
解决方案:通过 Associated Types 的 Typed Commands
步骤 1 —— 领域 newtypes
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Rpm(u32);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Volts(f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Watts(f64);
}
步骤 2 —— 命令 trait(GADT 等价物)
Associated type Response 是关键 —— 它将每个命令绑定到其返回类型:
#![allow(unused)]
fn main() {
trait IpmiCmd {
/// GADT "索引" —— 决定 execute() 返回什么。
type Response;
fn net_fn(&self) -> u8;
fn cmd_byte(&self) -> u8;
fn payload(&self) -> Vec<u8>;
/// 解析被封装在这里 —— 每个命令知道自己的字节布局。
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
}
步骤 3 —— 每个命令一个结构体,解析只写一次
#![allow(unused)]
fn main() {
struct ReadTemp { sensor_id: u8 }
impl IpmiCmd for ReadTemp {
type Response = Celsius; // ← "这个命令返回温度"
fn net_fn(&self) -> u8 { 0x04 }
fn cmd_byte(&self) -> u8 { 0x2D }
fn payload(&self) -> Vec<u8> { vec![self.sensor_id] }
fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
// 根据 IPMI SDR 有符号字节 —— 只写一次,测试一次
Ok(Celsius(raw[0] as i8 as f64))
}
}
struct ReadFanSpeed { fan_id: u8 }
impl IpmiCmd for ReadFanSpeed {
type Response = Rpm; // ← "这个命令返回 RPM"
fn net_fn(&self) -> u8 { 0x04 }
fn cmd_byte(&self) -> u8 { 0x2D }
fn payload(&self) -> Vec<u8> { vec![self.fan_id] }
fn parse_response(&self, raw: &[u8]) -> io::Result<Rpm> {
// 2 字节 LE —— 正确的布局,只编码一次
Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as u32))
}
}
struct ReadVoltage { rail: u8 }
impl IpmiCmd for ReadVoltage {
type Response = Volts; // ← "这个命令返回电压"
fn net_fn(&self) -> u8 { 0x04 }
fn cmd_byte(&self) -> u8 { 0x2D }
fn payload(&self) -> Vec<u8> { vec![self.rail] }
fn parse_response(&self, raw: &[u8]) -> io::Result<Volts> {
// 毫伏 → 伏特,总是正确
Ok(Volts(u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0))
}
}
struct ReadFru { fru_id: u8 }
impl IpmiCmd for ReadFru {
type Response = String;
fn net_fn(&self) -> u8 { 0x0A }
fn cmd_byte(&self) -> u8 { 0x11 }
fn payload(&self) -> Vec<u8> { vec![self.fru_id, 0x00, 0x00, 0xFF] }
fn parse_response(&self, raw: &[u8]) -> io::Result<String> {
Ok(String::from_utf8_lossy(raw).to_string())
}
}
}
步骤 4 —— 执行器(零 dyn,单态化)
#![allow(unused)]
fn main() {
struct BmcConnection { timeout_secs: u32 }
impl BmcConnection {
/// Generic 超过任何命令 —— 编译器为每个命令类型生成一个版本。
fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
let raw = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?;
cmd.parse_response(&raw)
}
fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result<Vec<u8>> {
Ok(vec![0x19, 0x00]) // 存根 —— 真正的实现调用 ipmitool
}
}
}
步骤 5 —— 调用者代码:所有四个 bug 都变成编译错误
#![allow(unused)]
fn main() {
fn diagnose_thermal(bmc: &BmcConnection) -> io::Result<()> {
let cpu_temp: Celsius = bmc.execute(&ReadTemp { sensor_id: 0x20 })?;
let fan_rpm: Rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 })?;
let voltage: Volts = bmc.execute(&ReadVoltage { rail: 0x40 })?;
// Bug #1 —— 不可能:解析住在 ReadFanSpeed::parse_response
// Bug #2 —— 不可能:缩放住在 ReadVoltage::parse_response
// Bug #3 —— 编译错误:
// if cpu_temp > fan_rpm { }
// ^^^^^^^^ ^^^^^^^
// Celsius Rpm → "类型不匹配" ❌
// Bug #4 —— 编译错误:
// log_temperature(voltage);
// ^^^^^^^ Volts, 期望 Celsius ❌
// 只有正确的比较编译:
if cpu_temp > Celsius(85.0) {
println!("CPU 过热:{:?}", cpu_temp);
}
if fan_rpm < Rpm(4000) {
println!("风扇太慢:{:?}", fan_rpm);
}
Ok(())
}
fn log_temperature(t: Celsius) { println!("Temp: {:?}", t); }
fn log_voltage(v: Volts) { println!("Voltage: {:?}", v); }
}
诊断脚本的宏 DSL
对于按顺序运行许多命令的大型诊断例程,宏提供简洁的声明式语法,同时保留完整的类型安全:
#![allow(unused)]
fn main() {
/// 执行一系列 typed IPMI 命令,返回结果元组。
/// 元组的每个元素都有命令自己的 Response 类型。
macro_rules! diag_script {
($bmc:expr; $($cmd:expr),+ $(,)?) => {{
( $( $bmc.execute(&$cmd)?, )+ )
}};
}
fn full_pre_flight(bmc: &BmcConnection) -> io::Result<()> {
// 展开为:(Celsius, Rpm, Volts, String) —— 每个类型都被跟踪
let (temp, rpm, volts, board_pn) = diag_script!(bmc;
ReadTemp { sensor_id: 0x20 },
ReadFanSpeed { fan_id: 0x30 },
ReadVoltage { rail: 0x40 },
ReadFru { fru_id: 0x00 },
);
println!("Board: {:?}", board_pn);
println!("CPU: {:?}, Fan: {:?}, 12V: {:?}", temp, rpm, volts);
// 类型安全的阈值检查:
assert!(temp < Celsius(95.0), "CPU 太热");
assert!(rpm > Rpm(3000), "风扇太慢");
assert!(volts > Volts(11.4), "12V rail 下降");
Ok(())
}
}
宏只是语法糖 —— 元组类型 (Celsius, Rpm, Volts, String) 完全由编译器推断。交换两个命令,解构在编译时失败,而不是运行时。
Enum Dispatch 用于异构命令列表
当你需要混合命令的 Vec(例如,从 JSON 加载的可配置脚本),使用 enum dispatch 保持无 dyn:
#![allow(unused)]
fn main() {
enum AnyReading {
Temp(Celsius),
Rpm(Rpm),
Volt(Volts),
Text(String),
}
enum AnyCmd {
Temp(ReadTemp),
Fan(ReadFanSpeed),
Voltage(ReadVoltage),
Fru(ReadFru),
}
impl AnyCmd {
fn execute(&self, bmc: &BmcConnection) -> io::Result<AnyReading> {
match self {
AnyCmd::Temp(c) => Ok(AnyReading::Temp(bmc.execute(c)?)),
AnyCmd::Fan(c) => Ok(AnyReading::Rpm(bmc.execute(c)?)),
AnyCmd::Voltage(c) => Ok(AnyReading::Volt(bmc.execute(c)?)),
AnyCmd::Fru(c) => Ok(AnyReading::Text(bmc.execute(c)?)),
}
}
}
/// 动态诊断脚本 —— 命令在运行时加载
fn run_script(bmc: &BmcConnection, script: &[AnyCmd]) -> io::Result<Vec<AnyReading>> {
script.iter().map(|cmd| cmd.execute(bmc)).collect()
}
}
你失去了每个元素的类型跟踪(一切都是 AnyReading),但获得了运行时灵活性 —— 解析仍然封装在每个 IpmiCmd 实现中。
测试 Typed Commands
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
struct StubBmc {
responses: std::collections::HashMap<u8, Vec<u8>>,
}
impl StubBmc {
fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
let key = cmd.payload()[0]; // 传感器 ID 作为 key
let raw = self.responses.get(&key)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no stub"))?;
cmd.parse_response(raw)
}
}
#[test]
fn read_temp_parses_signed_byte() {
let bmc = StubBmc {
responses: [( 0x20, vec![0xE7] )].into() // -25 作为 i8 = 0xE7
};
let temp = bmc.execute(&ReadTemp { sensor_id: 0x20 }).unwrap();
assert_eq!(temp, Celsius(-25.0));
}
#[test]
fn read_fan_parses_two_byte_le() {
let bmc = StubBmc {
responses: [( 0x30, vec![0x00, 0x19] )].into() // 0x1900 = 6400
};
let rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 }).unwrap();
assert_eq!(rpm, Rpm(6400));
}
#[test]
fn read_voltage_scales_millivolts() {
let bmc = StubBmc {
responses: [( 0x40, vec![0xE8, 0x2E] )].into() // 0x2EE8 = 12008 mV
};
let v = bmc.execute(&ReadVoltage { rail: 0x40 }).unwrap();
assert!((v.0 - 12.008).abs() < 0.001);
}
}
}
每个命令的解析被独立测试。如果 ReadFanSpeed 在新的 IPMI 规范修订中从 2 字节 LE 改为 4 字节 BE,你只需更新一个 parse_response,测试捕捉回归。
这如何映射到 Haskell GADTs
Haskell GADT Rust 等价物
──────────────── ───────────────────────
data Cmd a where trait IpmiCmd {
ReadTemp :: SensorId -> Cmd Temp type Response;
ReadFan :: FanId -> Cmd Rpm ...
}
eval :: Cmd a -> IO a fn execute<C: IpmiCmd>(&self, cmd: &C)
-> io::Result<C::Response>
类型细化在 case 分支中 单态化:编译器生成
execute::<ReadTemp>() → 返回 Celsius
execute::<ReadFanSpeed>() → 返回 Rpm
两者保证:命令决定返回类型。Rust 通过 generic 单态化实现它,而不是类型级 case 分析 —— 相同的安全性,零运行时成本。
前后对比总结
| 维度 | 未类型 (Vec<u8>) | Typed Commands |
|---|---|---|
| 每传感器行数 | ~3(在每个调用点重复) | ~15(只写和测试一次) |
| 可能的解析错误 | 在每个调用点 | 在一个 parse_response 实现中 |
| 单位混淆 bug | 无限 | 零(编译错误) |
| 添加新传感器 | 触摸 N 个文件,复制粘贴解析 | 添加 1 个结构体 + 1 个 impl |
| 运行时成本 | — | 相同(单态化) |
| IDE 自动补全 | 到处都是 f64 | Celsius、Rpm、Volts —— 自文档化 |
| 代码审查负担 | 必须验证每个原始字节解析 | 验证每个传感器一个 parse_response |
| 宏 DSL | N/A | diag_script!(bmc; ReadTemp{..}, ReadFan{..}) → (Celsius, Rpm) |
| 动态脚本 | 手动 dispatch | AnyCmd enum —— 仍然无 dyn |
何时使用 Typed Commands
| 场景 | 推荐 |
|---|---|
| IPMI 传感器读取带不同的物理单位 | ✅ Typed commands |
| 寄存器映射带不同宽度的字段 | ✅ Typed commands |
| 网络协议消息(请求 → 响应) | ✅ Typed commands |
| 单个命令类型带一个返回格式 | ❌ 过度 —— 直接返回类型 |
| 原型设计/探索未知设备 | ❌ 先用原始字节,后类型化 |
| 插件系统中命令在编译时未知 | ⚠️ 使用 AnyCmd enum dispatch |
关键要点 —— Traits
- Associated types = 每个类型一个实现;generic 参数 = 每个类型多个实现
- GATs 解锁 lending 迭代器和 async-in-traits 模式
- 对封闭集使用 enum dispatch(快);
dyn Trait用于开放集(灵活)Any+TypeId是当编译时类型未知时的逃逸出口
另见: 第 1 章 — Generics 了解单态化和何时 generics 导致代码膨胀。第 3 章 — Newtype & Type-State 了解如何在 config trait 模式中使用 traits。
练习:带 Associated Types 的 Repository ★★★(约 40 分钟)
设计一个 Repository trait,具有 associated Error、Id 和 Item 类型。为内存存储实现它并演示编译时类型安全。
🔑 解答
use std::collections::HashMap;
trait Repository {
type Item;
type Id;
type Error;
fn get(&self, id: &Self::Id) -> Result<Option<&Self::Item>, Self::Error>;
fn insert(&mut self, item: Self::Item) -> Result<Self::Id, Self::Error>;
fn delete(&mut self, id: &Self::Id) -> Result<bool, Self::Error>;
}
#[derive(Debug, Clone)]
struct User {
name: String,
email: String,
}
struct InMemoryUserRepo {
data: HashMap<u64, User>,
next_id: u64,
}
impl InMemoryUserRepo {
fn new() -> Self {
InMemoryUserRepo { data: HashMap::new(), next_id: 1 }
}
}
impl Repository for InMemoryUserRepo {
type Item = User;
type Id = u64;
type Error = std::convert::Infallible;
fn get(&self, id: &u64) -> Result<Option<&User>, Self::Error> {
Ok(self.data.get(id))
}
fn insert(&mut self, item: User) -> Result<u64, Self::Error> {
let id = self.next_id;
self.next_id += 1;
self.data.insert(id, item);
Ok(id)
}
fn delete(&mut self, id: &u64) -> Result<bool, Self::Error> {
Ok(self.data.remove(id).is_some())
}
}
fn create_and_fetch<R: Repository>(repo: &mut R, item: R::Item) -> Result<(), R::Error>
where
R::Item: std::fmt::Debug,
R::Id: std::fmt::Debug,
{
let id = repo.insert(item)?;
println!("Inserted with id: {id:?}");
let retrieved = repo.get(&id)?;
println!("Retrieved: {retrieved:?}");
Ok(())
}
fn main() {
let mut repo = InMemoryUserRepo::new();
create_and_fetch(&mut repo, User {
name: "Alice".into(),
email: "alice@example.com".into(),
}).unwrap();
}
3. Newtype 和 Type-State 模式 🟡
你将学到:
- Newtype 模式用于零成本编译时类型安全
- Type-state 模式:使非法状态转换无法表示
- 带类型状态的 builder 模式用于编译时强制的构造
- Config trait 模式用于控制 generic 参数爆炸
Newtype:零成本类型安全
Newtype 模式将现有类型包装在单字段元组结构体中,创建不同的类型,零运行时开销:
#![allow(unused)]
fn main() {
// 不用 newtype —— 容易搞混:
fn create_user(name: String, email: String, age: u32, employee_id: u32) { }
// create_user(name, email, age, id); — 但如果我们交换 age 和 id 呢?
// create_user(name, email, id, age); — 编译通过,BUG
// 用 newtype —— 编译器捕捉错误:
struct UserName(String);
struct Email(String);
struct Age(u32);
struct EmployeeId(u32);
fn create_user(name: UserName, email: Email, age: Age, id: EmployeeId) { }
// create_user(name, email, EmployeeId(42), Age(30));
// ❌ 编译错误:期望 AGE,得到 EmployeeId
}
impl Deref 用于 Newtypes —— 能力与陷阱
在 newtype 上实现 Deref 让它自动强制转换到内部类型的引用,“免费“获得内部类型的所有方法:
#![allow(unused)]
fn main() {
use std::ops::Deref;
struct Email(String);
impl Email {
fn new(raw: &str) -> Result<Self, &'static str> {
if raw.contains('@') {
Ok(Email(raw.to_string()))
} else {
Err("invalid email: missing @")
}
}
}
impl Deref for Email {
type Target = str;
fn deref(&self) -> &str { &self.0 }
}
// 现在 Email 自动解引用到 &str:
let email = Email::new("user@example.com").unwrap();
println!("Length: {}", email.len()); // 通过 Deref 使用 str::len
}
这很方便 —— 但它实际上在你的 newtype 抽象边界上打了一个洞,因为目标类型上的每个方法都可以在你的包装器上调用。
何时 Deref 是合适的
| 场景 | 示例 | 为什么可以 |
|---|---|---|
| 智能指针包装器 | Box<T>、Arc<T>、MutexGuard<T> | 包装器的目的就像 T 一样行为 |
| 透明“薄“包装器 | String → str、PathBuf → Path、Vec<T> → [T] | 包装器 IS-A 目标类型的超集 |
| 你的 newtype 确实是内部类型 | struct Hostname(String) 你总是想要完整的字符串操作 | 限制 API 没有增加价值 |
何时 Deref 是反模式
| 场景 | 问题 |
|---|---|
| 带不变量的领域类型 | Email 解引用到 &str,所以调用者可以调用 .split_at()、.trim() 等 —— 都不保持“必须包含 @“不变量。如果有人存储修剪后的 &str 并重建,不变量丢失。 |
| 想要限制 API 的类型 | struct Password(String) 带 Deref<Target = str> 泄露 .as_bytes()、.chars()、Debug 输出 —— 正是你试图隐藏的。 |
| 虚假继承 | 使用 Deref 让 ManagerWidget 自动解引用到 Widget 模拟 OOP 继承。这被明确不鼓励 —— 参见 Rust API Guidelines (C-DEREF)。 |
经验法则:如果你的 newtype 存在是为了添加类型安全或限制 API,不要实现
Deref。如果它存在是为了添加能力同时保持内部类型的完整表面(如智能指针),Deref是正确的选择。
DerefMut —— 风险加倍
如果你也实现 DerefMut,调用者可以直接可变借用内部值,绕过构造函数中的任何验证:
#![allow(unused)]
fn main() {
use std::ops::{Deref, DerefMut};
struct PortNumber(u16);
impl Deref for PortNumber {
type Target = u16;
fn deref(&self) -> &u16 { &self.0 }
}
impl DerefMut for PortNumber {
fn deref_mut(&mut self) -> &mut u16 { &mut self.0 }
}
let mut port = PortNumber(443);
*port = 0; // 绕过任何验证 —— 现在是无效端口
}
仅当内部类型没有要保护的不变量时才实现 DerefMut。
prefer 显式委托
当你只想要内部类型的一些方法时,显式委托:
#![allow(unused)]
fn main() {
struct Email(String);
impl Email {
fn new(raw: &str) -> Result<Self, &'static str> {
if raw.contains('@') { Ok(Email(raw.to_string())) }
else { Err("missing @") }
}
// 只暴露有意义的:
pub fn as_str(&self) -> &str { &self.0 }
pub fn len(&self) -> usize { self.0.len() }
pub fn domain(&self) -> &str {
self.0.split('@').nth(1).unwrap_or("")
}
// .split_at()、.trim()、.replace() —— 不暴露
}
}
Clippy 和生态系统
clippy::wrong_self_convention当Deref强制使方法解析令人惊讶时会触发(例如,is_empty()解析到内部类型的版本而不是你打算覆盖的)。- Rust API Guidelines (C-DEREF) 声明:“只有智能指针应该实现
Deref。” 将其视为强大的默认值;只有在有明确理由时才偏离。 - 如果你需要 trait 兼容性(例如,传递
Email给期望&str的函数),考虑实现AsRef<str>和Borrow<str>而不是 —— 它们是显式转换,无自动强制惊喜。
决策矩阵
你想要内部类型的 ALL 方法都可调用吗?
├── 是 → 你的类型执行不变量或限制 API 吗?
│ ├─ 否 → impl Deref ✅ (智能指针/透明包装器)
│ └─ 是 → 不要 impl Deref ❌ (不变量泄露)
└── 否 → 不要 impl Deref ❌ (使用 AsRef / 显式委托)
Type-State:编译时协议强制
Type-state 模式使用类型系统强制执行操作以正确的顺序发生。非法状态变得无法表示。
stateDiagram-v2
[*] --> Disconnected: new()
Disconnected --> Connected: connect()
Connected --> Authenticated: authenticate()
Authenticated --> Authenticated: request()
Authenticated --> [*]: drop
Disconnected --> Disconnected: ❌ request() 不会编译
Connected --> Connected: ❌ request() 不会编译
每个转换消耗
self并返回一个新类型 —— 编译器强制执行有效的顺序。
// 问题:一个网络连接必须:
// 1. 创建
// 2. 连接
// 3. 认证
// 4. 然后用于请求
// 在认证之前调用 request() 应该是编译错误。
// --- 类型状态标记(零大小类型)---
struct Disconnected;
struct Connected;
struct Authenticated;
// --- 按状态参数化的连接 ---
struct Connection<State> {
address: String,
_state: std::marker::PhantomData<State>,
}
// 只有 Disconnected 连接可以连接:
impl Connection<Disconnected> {
fn new(address: &str) -> Self {
Connection {
address: address.to_string(),
_state: std::marker::PhantomData,
}
}
fn connect(self) -> Connection<Connected> {
println!("连接到 {}...", self.address);
Connection {
address: self.address,
_state: std::marker::PhantomData,
}
}
}
// 只有 Connected 连接可以认证:
impl Connection<Connected> {
fn authenticate(self, _token: &str) -> Connection<Authenticated> {
println!("认证中...");
Connection {
address: self.address,
_state: std::marker::PhantomData,
}
}
}
// 只有 Authenticated 连接可以发出请求:
impl Connection<Authenticated> {
fn request(&self, path: &str) -> String {
format!("从 {} GET {}", path, self.address)
}
}
fn main() {
let conn = Connection::new("api.example.com");
// conn.request("/data"); // ❌ 编译错误:Connection<Disconnected> 上没有方法 `request`
let conn = conn.connect();
// conn.request("/data"); // ❌ 编译错误:Connection<Connected> 上没有方法 `request`
let conn = conn.authenticate("secret-token");
let response = conn.request("/data"); // ✅ 仅在认证后有效
println!("{response}");
}
关键洞察:每个状态转换消耗
self并返回一个新类型。 转换后不能使用旧状态 —— 编译器强制执行它。 零运行时成本 ——PhantomData是零大小的,状态在编译时被擦除。
与 C++/C# 对比:在 C++ 或 C# 中,你将通过运行时检查强制执行这个(if (!authenticated) throw ...)。Rust type-state 模式将这些检查移到编译时 —— 非法状态在类型系统中字面上无法表示。
带类型状态的 Builder 模式
一个实际应用 —— 一个强制 required 字段的 builder:
use std::marker::PhantomData;
// required 字段的标记类型
struct NeedsName;
struct NeedsPort;
struct Ready;
struct ServerConfig<State> {
name: Option<String>,
port: Option<u16>,
max_connections: usize, // 可选,有默认值
_state: PhantomData<State>,
}
impl ServerConfig<NeedsName> {
fn new() -> Self {
ServerConfig {
name: None,
port: None,
max_connections: 100,
_state: PhantomData,
}
}
fn name(self, name: &str) -> ServerConfig<NeedsPort> {
ServerConfig {
name: Some(name.to_string()),
port: self.port,
max_connections: self.max_connections,
_state: PhantomData,
}
}
}
impl ServerConfig<NeedsPort> {
fn port(self, port: u16) -> ServerConfig<Ready> {
ServerConfig {
name: self.name,
port: Some(port),
max_connections: self.max_connections,
_state: PhantomData,
}
}
}
impl ServerConfig<Ready> {
fn max_connections(mut self, n: usize) -> Self {
self.max_connections = n;
self
}
fn build(self) -> Server {
Server {
name: self.name.unwrap(),
port: self.port.unwrap(),
max_connections: self.max_connections,
}
}
}
struct Server {
name: String,
port: u16,
max_connections: usize,
}
fn main() {
// 必须提供 name,然后 port,然后可以 build:
let server = ServerConfig::new()
.name("my-server")
.port(8080)
.max_connections(500)
.build();
// ServerConfig::new().port(8080); // ❌ 编译错误:NeedsName 上没有方法 `port`
// ServerConfig::new().name("x").build(); // ❌ 编译错误:NeedsPort 上没有方法 `build`
}
案例研究:类型安全连接池
真实世界的系统需要连接池,其中连接通过定义良好的状态移动。这是 typestate 模式如何在生产池中强制执行正确性:
stateDiagram-v2
[*] --> Idle: pool.acquire()
Idle --> Active: conn.begin_transaction()
Active --> Active: conn.execute(query)
Active --> Idle: conn.commit() / conn.rollback()
Idle --> [*]: pool.release(conn)
Active --> [*]: ❌ 不能在事务中途释放
use std::marker::PhantomData;
// 状态
struct Idle;
struct InTransaction;
struct PooledConnection<State> {
id: u32,
_state: PhantomData<State>,
}
struct Pool {
next_id: u32,
}
impl Pool {
fn new() -> Self { Pool { next_id: 0 } }
fn acquire(&mut self) -> PooledConnection<Idle> {
self.next_id += 1;
println!("[pool] 获取连接 #{}", self.next_id);
PooledConnection { id: self.next_id, _state: PhantomData }
}
// 只有 idle 连接可以被释放 —— 防止事务中途泄露
fn release(&self, conn: PooledConnection<Idle>) {
println!("[pool] 释放连接 #{}", conn.id);
}
}
impl PooledConnection<Idle> {
fn begin_transaction(self) -> PooledConnection<InTransaction> {
println!("[conn #{}] BEGIN", self.id);
PooledConnection { id: self.id, _state: PhantomData }
}
}
impl PooledConnection<InTransaction> {
fn execute(&self, query: &str) {
println!("[conn #{}] EXEC: {}", self.id, query);
}
fn commit(self) -> PooledConnection<Idle> {
println!("[conn #{}] COMMIT", self.id);
PooledConnection { id: self.id, _state: PhantomData }
}
fn rollback(self) -> PooledConnection<Idle> {
println!("[conn #{}] ROLLBACK", self.id);
PooledConnection { id: self.id, _state: PhantomData }
}
}
fn main() {
let mut pool = Pool::new();
let conn = pool.acquire();
let conn = conn.begin_transaction();
conn.execute("INSERT INTO users VALUES ('Alice')");
conn.execute("INSERT INTO orders VALUES (1, 42)");
let conn = conn.commit(); // 回到 Idle
pool.release(conn); // ✅ 仅在 Idle 连接上有效
// pool.release(conn_active); // ❌ 编译错误:不能释放 InTransaction
}
为什么这在生产中很重要:事务中途泄露的连接无限期地持有数据库锁。Typestate 模式使这不可能 —— 你字面上不能返回连接到池,直到事务提交或回滚。
Config Trait 模式 —— 控制 Generic 参数爆炸
问题
当一个结构体承担更多责任时,每个都由 trait 约束的 generic 支持,类型签名变得笨拙:
#![allow(unused)]
fn main() {
trait SpiBus { fn spi_transfer(&self, tx: &[u8], rx: &mut [u8]) -> Result<(), BusError>; }
trait ComPort { fn com_send(&self, data: &[u8]) -> Result<usize, BusError>; }
trait I3cBus { fn i3c_read(&self, addr: u8, buf: &mut [u8]) -> Result<(), BusError>; }
trait SmBus { fn smbus_read_byte(&self, addr: u8, cmd: u8) -> Result<u8, BusError>; }
trait GpioBus { fn gpio_set(&self, pin: u32, high: bool); }
// ❌ 每个新总线 trait 添加另一个 generic 参数
struct DiagController<S: SpiBus, C: ComPort, I: I3cBus, M: SmBus, G: GpioBus> {
spi: S,
com: C,
i3c: I,
smbus: M,
gpio: G,
}
// impl 块、函数签名和调用者都重复完整列表。
// 添加第 6 个总线意味着编辑每个 DiagController<S, C, I, M, G> 的提及。
}
这通常被称为**“generic 参数爆炸。”**它在 impl 块、函数参数和下游消费者中复合 —— 每个都必须重复完整的参数列表。
解决方案:Config Trait
将所有 associated types 捆绑到一个单独的 trait 中。结构体然后只有一个 generic 参数,无论它包含多少组件类型:
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum BusError {
Timeout,
NakReceived,
HardwareFault(String),
}
// --- 总线 traits(不变)---
trait SpiBus {
fn spi_transfer(&self, tx: &[u8], rx: &mut [u8]) -> Result<(), BusError>;
fn spi_write(&self, data: &[u8]) -> Result<(), BusError>;
}
trait ComPort {
fn com_send(&self, data: &[u8]) -> Result<usize, BusError>;
fn com_recv(&self, buf: &mut [u8], timeout_ms: u32) -> Result<usize, BusError>;
}
trait I3cBus {
fn i3c_read(&self, addr: u8, buf: &mut [u8]) -> Result<(), BusError>;
fn i3c_write(&self, addr: u8, data: &[u8]) -> Result<(), BusError>;
}
// --- Config trait:每个组件一个 associated type ---
trait BoardConfig {
type Spi: SpiBus;
type Com: ComPort;
type I3c: I3cBus;
}
// --- DiagController 恰好有一个 generic 参数 ---
struct DiagController<Cfg: BoardConfig> {
spi: Cfg::Spi,
com: Cfg::Com,
i3c: Cfg::I3c,
}
}
DiagController<Cfg> 永远不会获得另一个 generic 参数。
添加第 4 个总线意味着添加一个 associated type 到 BoardConfig 和一个字段到 DiagController —— 无下游签名更改。
实现控制器
#![allow(unused)]
fn main() {
impl<Cfg: BoardConfig> DiagController<Cfg> {
fn new(spi: Cfg::Spi, com: Cfg::Com, i3c: Cfg::I3c) -> Self {
DiagController { spi, com, i3c }
}
fn read_flash_id(&self) -> Result<u32, BusError> {
let cmd = [0x9F]; // JEDEC Read ID
let mut id = [0u8; 4];
self.spi.spi_transfer(&cmd, &mut id)?;
Ok(u32::from_be_bytes(id))
}
fn send_bmc_command(&self, cmd: &[u8]) -> Result<Vec<u8>, BusError> {
self.com.com_send(cmd)?;
let mut resp = vec![0u8; 256];
let n = self.com.com_recv(&mut resp, 1000)?;
resp.truncate(n);
Ok(resp)
}
fn read_sensor_temp(&self, sensor_addr: u8) -> Result<i16, BusError> {
let mut buf = [0u8; 2];
self.i3c.i3c_read(sensor_addr, &mut buf)?;
Ok(i16::from_be_bytes(buf))
}
fn run_full_diag(&self) -> Result<DiagReport, BusError> {
let flash_id = self.read_flash_id()?;
let bmc_resp = self.send_bmc_command(b"VERSION\n")?;
let cpu_temp = self.read_sensor_temp(0x48)?;
let gpu_temp = self.read_sensor_temp(0x49)?;
Ok(DiagReport {
flash_id,
bmc_version: String::from_utf8_lossy(&bmc_resp).to_string(),
cpu_temp_c: cpu_temp,
gpu_temp_c: gpu_temp,
})
}
}
#[derive(Debug)]
struct DiagReport {
flash_id: u32,
bmc_version: String,
cpu_temp_c: i16,
gpu_temp_c: i16,
}
}
生产环境接线
一个 impl BoardConfig 选择具体的硬件驱动:
struct PlatformSpi { dev: String, speed_hz: u32 }
struct UartCom { dev: String, baud: u32 }
struct LinuxI3c { dev: String }
impl SpiBus for PlatformSpi {
fn spi_transfer(&self, tx: &[u8], rx: &mut [u8]) -> Result<(), BusError> {
// ioctl(SPI_IOC_MESSAGE) 在生产环境
rx[0..4].copy_from_slice(&[0xEF, 0x40, 0x18, 0x00]);
Ok(())
}
fn spi_write(&self, _data: &[u8]) -> Result<(), BusError> { Ok(()) }
}
impl ComPort for UartCom {
fn com_send(&self, _data: &[u8]) -> Result<usize, BusError> { Ok(0) }
fn com_recv(&self, buf: &mut [u8], _timeout: u32) -> Result<usize, BusError> {
let resp = b"BMC v2.4.1\n";
buf[..resp.len()].copy_from_slice(resp);
Ok(resp.len())
}
}
impl I3cBus for LinuxI3c {
fn i3c_read(&self, _addr: u8, buf: &mut [u8]) -> Result<(), BusError> {
buf[0] = 0x00; buf[1] = 0x2D; // 45°C
Ok(())
}
fn i3c_write(&self, _addr: u8, _data: &[u8]) -> Result<(), BusError> { Ok(()) }
}
// ✅ 一个结构体,一个 impl —— 所有具体类型在这里解析
struct ProductionBoard;
impl BoardConfig for ProductionBoard {
type Spi = PlatformSpi;
type Com = UartCom;
type I3c = LinuxI3c;
}
fn main() {
let ctrl = DiagController::<ProductionBoard>::new(
PlatformSpi { dev: "/dev/spidev0.0".into(), speed_hz: 10_000_000 },
UartCom { dev: "/dev/ttyS0".into(), baud: 115200 },
LinuxI3c { dev: "/dev/i3c-0".into() },
);
let report = ctrl.run_full_diag().unwrap();
println!("{report:#?}");
}
使用 Mock 测试接线
通过定义不同的 BoardConfig 交换整个硬件层:
#![allow(unused)]
fn main() {
struct MockSpi { flash_id: [u8; 4] }
struct MockCom { response: Vec<u8> }
struct MockI3c { temps: std::collections::HashMap<u8, i16> }
impl SpiBus for MockSpi {
fn spi_transfer(&self, _tx: &[u8], rx: &mut [u8]) -> Result<(), BusError> {
rx[..4].copy_from_slice(&self.flash_id);
Ok(())
}
fn spi_write(&self, _data: &[u8]) -> Result<(), BusError> { Ok(()) }
}
impl ComPort for MockCom {
fn com_send(&self, _data: &[u8]) -> Result<usize, BusError> { Ok(0) }
fn com_recv(&self, buf: &mut [u8], _timeout: u32) -> Result<usize, BusError> {
let n = self.response.len().min(buf.len());
buf[..n].copy_from_slice(&self.response[..n]);
Ok(n)
}
}
impl I3cBus for MockI3c {
fn i3c_read(&self, addr: u8, buf: &mut [u8]) -> Result<(), BusError> {
let temp = self.temps.get(&addr).copied().unwrap_or(0);
buf[..2].copy_from_slice(&temp.to_be_bytes());
Ok(())
}
fn i3c_write(&self, _addr: u8, _data: &[u8]) -> Result<(), BusError> { Ok(()) }
}
struct TestBoard;
impl BoardConfig for TestBoard {
type Spi = MockSpi;
type Com = MockCom;
type I3c = MockI3c;
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_controller() -> DiagController<TestBoard> {
let mut temps = std::collections::HashMap::new();
temps.insert(0x48, 45i16);
temps.insert(0x49, 72i16);
DiagController::<TestBoard>::new(
MockSpi { flash_id: [0xEF, 0x40, 0x18, 0x00] },
MockCom { response: b"BMC v2.4.1\n".to_vec() },
MockI3c { temps },
)
}
#[test]
fn test_flash_id() {
let ctrl = make_test_controller();
assert_eq!(ctrl.read_flash_id().unwrap(), 0xEF401800);
}
#[test]
fn test_sensor_temps() {
let ctrl = make_test_controller();
assert_eq!(ctrl.read_sensor_temp(0x48).unwrap(), 45);
assert_eq!(ctrl.read_sensor_temp(0x49).unwrap(), 72);
}
#[test]
fn test_full_diag() {
let ctrl = make_test_controller();
let report = ctrl.run_full_diag().unwrap();
assert_eq!(report.flash_id, 0xEF401800);
assert_eq!(report.cpu_temp_c, 45);
assert_eq!(report.gpu_temp_c, 72);
assert!(report.bmc_version.contains("2.4.1"));
}
}
}
稍后添加新总线
当你需要第 4 个总线时,只有两件事更改 —— BoardConfig 和 DiagController。
无下游签名更改。 Generic 参数计数保持为 1:
#![allow(unused)]
fn main() {
trait SmBus {
fn smbus_read_byte(&self, addr: u8, cmd: u8) -> Result<u8, BusError>;
}
// 1. 添加一个 associated type:
trait BoardConfig {
type Spi: SpiBus;
type Com: ComPort;
type I3c: I3cBus;
type Smb: SmBus; // ← 新增
}
// 2. 添加一个字段:
struct DiagController<Cfg: BoardConfig> {
spi: Cfg::Spi,
com: Cfg::Com,
i3c: Cfg::I3c,
smb: Cfg::Smb, // ← 新增
}
// 3. 在每个 config impl 中提供具体类型:
impl BoardConfig for ProductionBoard {
type Spi = PlatformSpi;
type Com = UartCom;
type I3c = LinuxI3c;
type Smb = LinuxSmbus; // ← 新增
}
}
何时使用这个模式
| 情况 | 使用 Config Trait? | 替代方案 |
|---|---|---|
| 结构体上 3+ trait 约束 generics | ✅ 是 | — |
| 需要交换整个硬件/平台层 | ✅ 是 | — |
| 只有 1-2 个 generics | ❌ 过度 | 直接 generics |
| 需要运行时多态 | ❌ | dyn Trait 对象 |
| 开放结束的插件系统 | ❌ | Type-map / Any |
| 组件 traits 形成自然组(板卡、平台) | ✅ 是 | — |
关键属性
- 永远一个 generic 参数 ——
DiagController<Cfg>永远不会获得更多<A, B, C, ...> - 完全静态 dispatch —— 无 vtables、无
dyn、无堆分配用于 trait 对象 - 干净测试交换 —— 定义
TestBoard带 mock impls,零条件编译 - 编译时安全 —— 忘记一个 associated type → 编译错误,不是运行时崩溃
- 久经验证 —— 这是 Substrate/Polkadot 的 frame 系统使用的模式,通过单个
Configtrait 管理 20+ associated types
关键要点 —— Newtype & Type-State
- Newtypes 提供编译时类型安全,零运行时成本
- Type-state 使非法状态转换成为编译错误,不是运行时 bug
- Config traits 控制大型系统中的 generic 参数爆炸
另见: 第 4 章 — PhantomData 了解驱动 type-state 的零大小标记。第 2 章 — Traits 深入 了解 config trait 模式中使用的 associated types。
案例研究:双轴 Typestate —— Vendor × 协议状态
上面的模式一次处理一个轴:typestate 强制执行协议顺序,trait 抽象处理多个 vendors。真实系统通常需要两者同时:一个包装器 Handle<Vendor, State> 其中可用方法取决于哪个 vendor被插入和 哪个状态 handle 处于。
本节展示双轴条件 impl 模式 —— 其中 impl 块被 gating 在 vendor trait bound 和状态标记 trait 上。
二维问题
考虑一个调试探针接口(JTAG/SWD)。多个厂商制造探针,每个探针必须在使用前解锁。一些厂商额外支持直接内存读取 —— 但仅在扩展解锁配置内存访问端口后:
graph LR
subgraph "所有厂商"
L["🔒 Locked"] -- "unlock()" --> U["🔓 Unlocked"]
end
subgraph "支持内存的厂商"
U -- "extended_unlock()" --> E["🔓🧠 ExtendedUnlocked"]
end
U -. "read_reg() / write_reg()" .-> U
E -. "read_reg() / write_reg()" .-> E
E -. "read_memory() / write_memory()" .-> E
style L fill:#fee,stroke:#c33
style U fill:#efe,stroke:#3a3
style E fill:#eef,stroke:#33c
能力矩阵 —— 哪些方法存在于哪些 (vendor, state) 组合 —— 是二维的:
block-beta
columns 4
space header1["Locked"] header2["Unlocked"] header3["ExtendedUnlocked"]
basic["Basic Vendor"]:1 b1["unlock()"] b2["read_reg()<br/>write_reg()"] b3["— unreachable —"]
memory["Memory Vendor"]:1 m1["unlock()"] m2["read_reg()<br/>write_reg()<br/>extended_unlock()"] m3["read_reg()<br/>write_reg()<br/>read_memory()<br/>write_memory()"]
style b1 fill:#ffd,stroke:#aa0
style b2 fill:#efe,stroke:#3a3
style b3 fill:#eee,stroke:#999,stroke-dasharray: 5 5
style m1 fill:#ffd,stroke:#aa0
style m2 fill:#efe,stroke:#3a3
style m3 fill:#eef,stroke:#33c
挑战:完全在编译时表达这个矩阵,带静态 dispatch,使得在基本探针上调用 extended_unlock() 或在解锁但未扩展的 handle 上调用 read_memory() 是编译错误。
解决方案:带标记 Traits 的 Jtag<V, S>
步骤 1 —— 状态令牌和能力标记:
use std::marker::PhantomData;
// 零大小状态令牌 —— 无运行时成本
struct Locked;
struct Unlocked;
struct ExtendedUnlocked;
// 标记 traits 表达哪个状态有哪个能力
trait HasRegAccess {}
impl HasRegAccess for Unlocked {}
impl HasRegAccess for ExtendedUnlocked {}
trait HasMemAccess {}
impl HasMemAccess for ExtendedUnlocked {}
为什么是标记 traits,不是具体状态? 编写
impl<V, S: HasRegAccess> Jtag<V, S>意味着read_reg()可用于任何有寄存器访问的状态 —— 今天是Unlocked和ExtendedUnlocked,但如果你明天添加DebugHalted,你只需添加一行:impl HasRegAccess for DebugHalted {}。每个寄存器函数自动与它一起工作 —— 零代码更改。
步骤 2 —— Vendor traits(原始操作):
// 每个探针厂商实现这些
trait JtagVendor {
fn raw_unlock(&mut self);
fn raw_read_reg(&self, addr: u32) -> u32;
fn raw_write_reg(&mut self, addr: u32, val: u32);
}
// 带内存访问的厂商也实现这个 super-trait
trait JtagMemoryVendor: JtagVendor {
fn raw_extended_unlock(&mut self);
fn raw_read_memory(&self, addr: u64, buf: &mut [u8]);
fn raw_write_memory(&mut self, addr: u64, data: &[u8]);
}
步骤 3 —— 带条件 impl 块的包装器:
struct Jtag<V, S = Locked> {
vendor: V,
_state: PhantomData<S>,
}
// 构造 —— 总是从 Locked 开始
impl<V: JtagVendor> Jtag<V, Locked> {
fn new(vendor: V) -> Self {
Jtag { vendor, _state: PhantomData }
}
fn unlock(mut self) -> Jtag<V, Unlocked> {
self.vendor.raw_unlock();
Jtag { vendor: self.vendor, _state: PhantomData }
}
}
// 寄存器 I/O —— 任何厂商,任何带 HasRegAccess 的状态
impl<V: JtagVendor, S: HasRegAccess> Jtag<V, S> {
fn read_reg(&self, addr: u32) -> u32 {
self.vendor.raw_read_reg(addr)
}
fn write_reg(&mut self, addr: u32, val: u32) {
self.vendor.raw_write_reg(addr, val);
}
}
// 扩展解锁 —— 仅内存能力厂商,仅从 Unlocked
impl<V: JtagMemoryVendor> Jtag<V, Unlocked> {
fn extended_unlock(mut self) -> Jtag<V, ExtendedUnlocked> {
self.vendor.raw_extended_unlock();
Jtag { vendor: self.vendor, _state: PhantomData }
}
}
// 内存 I/O —— 仅内存能力厂商,仅 ExtendedUnlocked
impl<V: JtagMemoryVendor, S: HasMemAccess> Jtag<V, S> {
fn read_memory(&self, addr: u64, buf: &mut [u8]) {
self.vendor.raw_read_memory(addr, buf);
}
fn write_memory(&mut self, addr: u64, data: &[u8]) {
self.vendor.raw_write_memory(addr, data);
}
}
每个 impl 块编码能力矩阵的一个单元格(或行)。
编译器强制执行矩阵 —— 无处运行时检查。
厂商实现
添加厂商意味着在一个结构体上实现原始方法 —— 无每个状态的结构体重复,无委托样板代码:
// 厂商 A:基本探针 —— 仅寄存器访问
struct BasicProbe { port: u16 }
impl JtagVendor for BasicProbe {
fn raw_unlock(&mut self) { /* TAP reset sequence */ }
fn raw_read_reg(&self, addr: u32) -> u32 { /* DR scan */ 0 }
fn raw_write_reg(&mut self, addr: u32, val: u32) { /* DR scan */ }
}
// BasicProbe 不 impl JtagMemoryVendor。
// extended_unlock() 不会在 Jtag<BasicProbe, _> 上编译。
// 厂商 B:全功能探针 —— 寄存器 + 内存
struct DapProbe { serial: String }
impl JtagVendor for DapProbe {
fn raw_unlock(&mut self) { /* SWD switch, read DPIDR */ }
fn raw_read_reg(&self, addr: u32) -> u32 { /* AP register read */ 0 }
fn raw_write_reg(&mut self, addr: u32, val: u32) { /* AP register write */ }
}
impl JtagMemoryVendor for DapProbe {
fn raw_extended_unlock(&mut self) { /* select MEM-AP, power up */ }
fn raw_read_memory(&self, addr: u64, buf: &mut [u8]) { /* MEM-AP read */ }
fn raw_write_memory(&mut self, addr: u64, data: &[u8]) { /* MEM-AP write */ }
}
编译器防止什么
| 尝试 | 错误 | 为什么 |
|---|---|---|
Jtag<_, Locked>::read_reg() | 无方法 read_reg | Locked 不 impl HasRegAccess |
Jtag<BasicProbe, _>::extended_unlock() | 无方法 extended_unlock | BasicProbe 不 impl JtagMemoryVendor |
Jtag<_, Unlocked>::read_memory() | 无方法 read_memory | Unlocked 不 impl HasMemAccess |
调用 unlock() 两次 | 值在移动后使用 | unlock() 消耗 self |
所有四个错误都被捕获在编译时。无 panics、无 Option、无运行时状态 enum。
编写 Generic 函数
函数绑定它们关心的轴:
/// 适用于 ANY vendor,ANY 带 register access 的状态。
fn read_idcode<V: JtagVendor, S: HasRegAccess>(jtag: &Jtag<V, S>) -> u32 {
jtag.read_reg(0x00)
}
/// 仅适用于 ExtendedUnlocked 状态的内存能力厂商。
fn dump_firmware<V: JtagMemoryVendor, S: HasMemAccess>(jtag: &Jtag<V, S>) {
let mut buf = [0u8; 256];
jtag.read_memory(0x0800_0000, &mut buf);
}
read_idcode 不关心你是否在 Unlocked 或 ExtendedUnlocked ——
它只要求 HasRegAccess。这是标记 traits 超越标记特定状态在签名中的优势。
相同模式,不同领域:Storage Backends
双轴技术不是硬件特定的。这是相同的结构用于存储层,其中一些 backends 支持事务:
// 状态
struct Closed;
struct Open;
struct InTransaction;
trait HasReadWrite {}
impl HasReadWrite for Open {}
impl HasReadWrite for InTransaction {}
// Vendor traits
trait StorageBackend {
fn raw_open(&mut self);
fn raw_read(&self, key: &[u8]) -> Option<Vec<u8>>;
fn raw_write(&mut self, key: &[u8], value: &[u8]);
}
trait TransactionalBackend: StorageBackend {
fn raw_begin(&mut self);
fn raw_commit(&mut self);
fn raw_rollback(&mut self);
}
// 包装器
struct Store<B, S = Closed> { backend: B, _s: PhantomData<S> }
impl<B: StorageBackend> Store<B, Closed> {
fn open(mut self) -> Store<B, Open> { self.backend.raw_open(); /* ... */ todo!() }
}
impl<B: StorageBackend, S: HasReadWrite> Store<B, S> {
fn read(&self, key: &[u8]) -> Option<Vec<u8>> { self.backend.raw_read(key) }
fn write(&mut self, key: &[u8], val: &[u8]) { self.backend.raw_write(key, val) }
}
impl<B: TransactionalBackend> Store<B, Open> {
fn begin(mut self) -> Store<B, InTransaction> { /* ... */ todo!() }
}
impl<B: TransactionalBackend> Store<B, InTransaction> {
fn commit(mut self) -> Store<B, Open> { /* ... */ todo!() }
fn rollback(mut self) -> Store<B, Open> { /* ... */ todo!() }
}
平板文件 backend 只实现 StorageBackend —— begin() 不会编译。数据库 backend 添加 TransactionalBackend —— 完整 Open → InTransaction → Open 周期变得可用。
何时使用这个模式
| 信号 | 为什么双轴适合 |
|---|---|
| 两个独立轴:“谁提供它” 和 “它处于什么状态” | impl 块矩阵直接编码两者 |
| 一些提供者严格比其他有更多能力 | Super-trait (MemoryVendor: Vendor) + 条件 impl |
| 误用状态或能力是安全/正确性 bug | 编译时预防 > 运行时检查 |
| 想要静态 dispatch(无 vtables) | PhantomData + generics = 零成本 |
| 信号 | 考虑更简单的方案 |
|---|---|
| 只有一个轴变化(状态 OR vendor,不是两者) | 单轴 typestate 或普通 trait 对象 |
| 三个或更多独立轴 | Config Trait Pattern 捆绑轴到 associated types |
| 运行时多态可接受 | enum 状态 + dyn dispatch 更简单 |
当两个轴变成三个或更多: 如果你发现自己编写
Handle<V, S, D, T>—— vendor、state、debug 级别、transport —— generic 参数列表在告诉你一些事情。 考虑折叠vendor轴到 associated-type config trait(本章前面的 Config Trait Pattern),仅保留状态轴作为 generic 参数:Handle<Cfg, S>。Config trait 捆绑type Vendor、type Transport等。到一个参数,状态轴保留其编译时转换保证。 这是一个自然演进,不是重写 —— 你将 vendor 相关类型提升到Cfg中,保持 typestate 机制 untouched。
关键要点: 双轴模式是 typestate 和 trait 基于抽象的交集。每个
impl块映射到 (vendor × state) 矩阵的一个单元格。编译器强制执行整个矩阵 —— 无运行时状态检查、无不可能状态 panics、无成本。
练习:类型安全状态机 ★★(约 30 分钟)
使用 type-state 模式构建一个交通灯状态机。灯必须按 Red → Green → Yellow → Red 转换,不允许其他顺序。
🔑 解答
use std::marker::PhantomData;
struct Red;
struct Green;
struct Yellow;
struct TrafficLight<State> {
_state: PhantomData<State>,
}
impl TrafficLight<Red> {
fn new() -> Self {
println!("🔴 Red — STOP");
TrafficLight { _state: PhantomData }
}
fn go(self) -> TrafficLight<Green> {
println!("🟢 Green — GO");
TrafficLight { _state: PhantomData }
}
}
impl TrafficLight<Green> {
fn caution(self) -> TrafficLight<Yellow> {
println!("🟡 Yellow — CAUTION");
TrafficLight { _state: PhantomData }
}
}
impl TrafficLight<Yellow> {
fn stop(self) -> TrafficLight<Red> {
println!("🔴 Red — STOP");
TrafficLight { _state: PhantomData }
}
}
fn main() {
let light = TrafficLight::new(); // Red
let light = light.go(); // Green
let light = light.caution(); // Yellow
let _light = light.stop(); // Red
// light.caution(); // ❌ 编译错误:Red 上没有方法 `caution`
// TrafficLight::new().stop(); // ❌ 编译错误:Red 上没有方法 `stop`
}
关键要点:非法转换是编译错误,不是运行时 panics。
4. PhantomData —— 不携带数据的类型 🔴
你将学到:
- 为什么
PhantomData<T>存在以及它解决的三个问题- 用于编译时作用域强制的生命周期品牌化
- 用于维度安全算术的单位模式
- 方差(协变、逆变、不变)以及 PhantomData 如何控制它
PhantomData 解决什么
PhantomData<T> 是一个零大小类型,告诉编译器“这个结构体在逻辑上与 T 关联,即使它不包含 T。“它影响方差、drop 检查和自动 trait 推断 —— 不使用任何内存。
#![allow(unused)]
fn main() {
use std::marker::PhantomData;
// 不用 PhantomData:
struct Slice<'a, T> {
ptr: *const T,
len: usize,
// 问题:编译器不知道这个结构体从 'a 借用
// 或者它与 T 在 drop 检查目的上关联
}
// 用 PhantomData:
struct Slice<'a, T> {
ptr: *const T,
len: usize,
_marker: PhantomData<&'a T>,
// 现在编译器知道:
// 1. 这个结构体借用生命周期 'a 的数据
// 2. 它在 'a 上是协变的(生命周期可以缩短)
// 3. Drop 检查考虑 T
}
}
PhantomData 的三个工作:
| 工作 | 示例 | 它做什么 |
|---|---|---|
| 生命周期绑定 | PhantomData<&'a T> | 结构体被视为借用 'a |
| 所有权模拟 | PhantomData<T> | Drop 检查假设结构体拥有 T |
| 方差控制 | PhantomData<fn(T)> | 使结构体在 T 上逆变 |
生命周期品牌化
使用 PhantomData 防止混合来自不同“会话“或“上下文“的值:
use std::marker::PhantomData;
/// 一个 handle 仅在特定 arena 的生命周期内有效
struct ArenaHandle<'arena> {
index: usize,
_brand: PhantomData<&'arena ()>,
}
struct Arena {
data: Vec<String>,
}
impl Arena {
fn new() -> Self {
Arena { data: Vec::new() }
}
/// 分配一个字符串并返回一个品牌化的 handle
fn alloc<'a>(&'a mut self, value: String) -> ArenaHandle<'a> {
let index = self.data.len();
self.data.push(value);
ArenaHandle { index, _brand: PhantomData }
}
/// 通过 handle 查找 —— 只接受来自 THIS arena 的 handles
fn get<'a>(&'a self, handle: ArenaHandle<'a>) -> &'a str {
&self.data[handle.index]
}
}
fn main() {
let mut arena1 = Arena::new();
let handle1 = arena1.alloc("hello".to_string());
// 不能在不同的 arena 上使用 handle1 —— 生命周期不匹配
// let mut arena2 = Arena::new();
// arena2.get(handle1); // ❌ 生命周期不匹配
println!("{}", arena1.get(handle1)); // ✅
}
单位模式
在编译时防止混合不兼容的单位,零运行时成本:
use std::marker::PhantomData;
use std::ops::{Add, Mul};
// 单位标记类型(零大小)
struct Meters;
struct Seconds;
struct MetersPerSecond;
#[derive(Debug, Clone, Copy)]
struct Quantity<Unit> {
value: f64,
_unit: PhantomData<Unit>,
}
impl<U> Quantity<U> {
fn new(value: f64) -> Self {
Quantity { value, _unit: PhantomData }
}
}
// 只能添加相同单位:
impl<U> Add for Quantity<U> {
type Output = Quantity<U>;
fn add(self, rhs: Self) -> Self::Output {
Quantity::new(self.value + rhs.value)
}
}
// Meters / Seconds = MetersPerSecond(自定义 trait)
impl std::ops::Div<Quantity<Seconds>> for Quantity<Meters> {
type Output = Quantity<MetersPerSecond>;
fn div(self, rhs: Quantity<Seconds>) -> Quantity<MetersPerSecond> {
Quantity::new(self.value / rhs.value)
}
}
fn main() {
let dist = Quantity::<Meters>::new(100.0);
let time = Quantity::<Seconds>::new(9.58);
let speed = dist / time; // Quantity<MetersPerSecond>
println!("Speed: {:.2} m/s", speed.value); // 10.44 m/s
// let nonsense = dist + time; // ❌ 编译错误:不能添加 Meters + Seconds
}
这是纯粹的类型系统魔法 ——
PhantomData<Meters>是零大小的, 所以Quantity<Meters>与f64有相同的布局。运行时无包装器开销, 但编译时有完整的单位安全。
PhantomData 和 Drop Check
当编译器检查结构体的析构函数是否可能访问过期的数据时,它使用 PhantomData 来决定:
#![allow(unused)]
fn main() {
use std::marker::PhantomData;
// PhantomData<T> —— 编译器假设我们 MAYBE drop 一个 T
// 这意味着 T 必须比我们的结构体活得长
struct OwningSemantic<T> {
ptr: *const T,
_marker: PhantomData<T>, // "我逻辑上拥有一个 T"
}
// PhantomData<*const T> —— 编译器假设我们不拥有 T
// 更宽松 —— T 不需要比我们活得长
struct NonOwningSemantic<T> {
ptr: *const T,
_marker: PhantomData<*const T>, // "我只是指向 T"
}
}
实用规则:当包装原始指针时,仔细选择 PhantomData:
- 编写拥有其数据的容器? →
PhantomData<T> - 编写视图/引用类型? →
PhantomData<&'a T>或PhantomData<*const T>
方差 —— 为什么 PhantomData 的类型参数很重要
方差决定 generic 类型是否可以被 sub- 或 super-type 替换(在 Rust 中,“subtype“意味着“有更长的生命周期”。方差错误会导致拒绝好代码或接受不 sound 的代码。
graph LR
subgraph Covariant
direction TB
A1["&'long T"] -->|"can become"| A2["&'short T"]
end
subgraph Contravariant
direction TB
B1["fn(&'short T)"] -->|"can become"| B2["fn(&'long T)"]
end
subgraph Invariant
direction TB
C1["&'a mut T"] ---|"NO substitution"| C2["&'b mut T"]
end
style A1 fill:#d4efdf,stroke:#27ae60,color:#000
style A2 fill:#d4efdf,stroke:#27ae60,color:#000
style B1 fill:#e8daef,stroke:#8e44ad,color:#000
style B2 fill:#e8daef,stroke:#8e44ad,color:#000
style C1 fill:#fadbd8,stroke:#e74c3c,color:#000
style C2 fill:#fadbd8,stroke:#e74c3c,color:#000
三种方差
| 方差 | 含义 | “我可以替换…” | Rust 示例 |
|---|---|---|---|
| Covariant | Subtype 流入 | 'long where 'short expected ✅ | &'a T、Vec<T>、Box<T> |
| Contravariant | Subtype 反向流 | 'short where 'long expected ✅ | fn(T)(在参数位置) |
| Invariant | 不允许替换 | 两个方向都不 ✅ | &mut T、Cell<T>、UnsafeCell<T> |
为什么 &'a T 在 'a 上是协变的
fn print_str(s: &str) {
println!("{s}");
}
fn main() {
let owned = String::from("hello");
// owned 活得长('long)
// print_str 期望 &'_ str('short —— 仅用于调用)
print_str(&owned); // ✅ 协变:'long → 'short 是安全的
// 更长生命周期的引用总是可以在需要较短生命周期的地方使用。
}
为什么 &mut T 在 T 上是不变的
#![allow(unused)]
fn main() {
// 如果 &mut T 在 T 上是协变的,这将编译:
fn evil(s: &mut &'static str) {
// 我们可以将一个较短生命周期的 &str 写入 &'static str 槽!
let local = String::from("temporary");
// *s = &local; // ← 将创建一个悬垂的 &'static str
}
// 不变性防止这个:&'static str ≠ &'a str 当可变时。
// 编译器完全拒绝替换。
}
PhantomData 如何控制方差
PhantomData<X> 给你的结构体与 X 相同的方差:
#![allow(unused)]
fn main() {
use std::marker::PhantomData;
// 在 'a 上协变 —— 一个 Ref<'long> 可以作为 Ref<'short> 使用
struct Ref<'a, T> {
ptr: *const T,
_marker: PhantomData<&'a T>, // 在 'a 上协变,在 T 上协变
}
// 在 T 上不变 —— 防止 T 的不 sound 生命周期缩短
struct MutRef<'a, T> {
ptr: *mut T,
_marker: PhantomData<&'a mut T>, // 在 'a 上协变,在 T 上**不变**
}
// 在 T 上逆变 —— 用于回调容器
struct CallbackSlot<T> {
_marker: PhantomData<fn(T)>, // 在 T 上逆变
}
}
PhantomData 方差速查表:
| PhantomData 类型 | 在 T 上的方差 | 在 'a 上的方差 | 何时使用 |
|---|---|---|---|
PhantomData<T> | 协变 | — | 你逻辑上拥有 T |
PhantomData<&'a T> | 协变 | 协变 | 你借用生命周期 'a 的 T |
PhantomData<&'a mut T> | 不变 | 协变 | 你可变借用 T |
PhantomData<*const T> | 协变 | — | 非拥有指针指向 T |
PhantomData<*mut T> | 不变 | — | 非拥有可变指针 |
PhantomData<fn(T)> | 逆变 | — | T 出现在参数位置 |
PhantomData<fn() -> T> | 协变 | — | T 出现在返回位置 |
PhantomData<fn(T) -> T> | 不变 | — | T 在两个位置抵消 |
实际示例:为什么这在实际中很重要
use std::marker::PhantomData;
// 一个令牌,用会话生命周期品牌价值。
// 必须在 'a 上是协变的 —— 否则调用者不能缩短
// 传递给需要较短借用的函数时的生命周期。
struct SessionToken<'a> {
id: u64,
_brand: PhantomData<&'a ()>, // ✅ 协变 —— 调用者可以缩短 'a
// _brand: PhantomData<fn(&'a ())>, // ❌ 逆变 —— 破坏可用性
// _brand: PhantomData<&'a mut ()>; // 仍在 'a 上协变(在 T 上不变,但 T 固定为 ())
}
fn use_token(token: &SessionToken<'_>) {
println!("使用令牌 {}", token.id);
}
fn main() {
let token = SessionToken { id: 42, _brand: PhantomData };
use_token(&token); // ✅ 有效因为 SessionToken 在 'a 上是协变的
}
决策规则:从
PhantomData<&'a T>(协变)开始。仅当你的抽象 分发对T的可变访问时才切换到PhantomData<&'a mut T>(不变)。 几乎从不使用PhantomData<fn(T)>(逆变) —— 它仅对回调存储场景正确。
关键要点 —— PhantomData
PhantomData<T>携带类型/生命周期信息,无运行时成本- 用于生命周期品牌化、方差控制和单位模式
- Drop 检查:
PhantomData<T>告诉编译器你的类型逻辑上拥有T
另见: 第 3 章 — Newtype & Type-State 了解使用 PhantomData 的 type-state 模式。第 12 章 — Unsafe Rust 了解 PhantomData 如何与原始指针交互。
练习:带 PhantomData 的单位模式 ★★(约 30 分钟)
扩展单位模式以支持:
Meters、Seconds、Kilograms- 相同单位的加法
- 乘法:
Meters * Meters = SquareMeters - 除法:
Meters / Seconds = MetersPerSecond
🔑 解答
use std::marker::PhantomData;
use std::ops::{Add, Mul, Div};
#[derive(Clone, Copy)]
struct Meters;
#[derive(Clone, Copy)]
struct Seconds;
#[derive(Clone, Copy)]
struct Kilograms;
#[derive(Clone, Copy)]
struct SquareMeters;
#[derive(Clone, Copy)]
struct MetersPerSecond;
#[derive(Debug, Clone, Copy)]
struct Qty<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Qty<U> {
fn new(v: f64) -> Self { Qty { value: v, _unit: PhantomData } }
}
impl<U> Add for Qty<U> {
type Output = Qty<U>;
fn add(self, rhs: Self) -> Self::Output { Qty::new(self.value + rhs.value) }
}
impl Mul<Qty<Meters>> for Qty<Meters> {
type Output = Qty<SquareMeters>;
fn mul(self, rhs: Qty<Meters>) -> Qty<SquareMeters> {
Qty::new(self.value * rhs.value)
}
}
impl Div<Qty<Seconds>> for Qty<Meters> {
type Output = Qty<MetersPerSecond>;
fn div(self, rhs: Qty<Seconds>) -> Qty<MetersPerSecond> {
Qty::new(self.value / rhs.value)
}
}
fn main() {
let width = Qty::<Meters>::new(5.0);
let height = Qty::<Meters>::new(3.0);
let area = width * height; // Qty<SquareMeters>
println!("Area: {:.1} m²", area.value);
let dist = Qty::<Meters>::new(100.0);
let time = Qty::<Seconds>::new(9.58);
let speed = dist / time;
println!("Speed: {:.2} m/s", speed.value);
let sum = width + height; // 相同单位 ✅
println!("Sum: {:.1} m", sum.value);
// let bad = width + time; // ❌ 编译错误:不能添加 Meters + Seconds
}
5. Channels and Message Passing 🟢
你将学到:
std::sync::mpsc基础以及何时升级到 crossbeam-channel- 使用
select!进行 channel 选择,处理多源消息- 有界 vs 无界 channel 和背压策略
- 用于封装并发状态的 actor 模式
std::sync::mpsc — 标准 Channel
Rust 标准库提供了一个多生产者、单消费者的 channel:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// 创建一个 channel:tx(发送者)和 rx(接收者)
let (tx, rx) = mpsc::channel();
// 生成一个生产者线程
let tx1 = tx.clone(); // 克隆以支持多生产者
thread::spawn(move || {
for i in 0..5 {
tx1.send(format!("producer-1: msg {i}")).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
// 第二个生产者
thread::spawn(move || {
for i in 0..5 {
tx.send(format!("producer-2: msg {i}")).unwrap();
thread::sleep(Duration::from_millis(150));
}
});
// 消费者:接收所有消息
for msg in rx {
// 当所有发送者都 drop 时,rx 迭代器结束
println!("Received: {msg}");
}
println!("All producers done.");
}
注意:
.send()上的.unwrap()是为了简洁。如果接收者已被 drop,它会 panic。生产代码应该优雅地处理SendError。
关键属性:
- 默认无界(如果消费者慢,可能填满内存)
mpsc::sync_channel(N)创建一个有界 channel,具有背压rx.recv()阻塞当前线程,直到消息到达rx.try_recv()立即返回,如果没有准备好则返回Err(TryRecvError::Empty)- 当所有
Sender都被 drop 时,channel 关闭
#![allow(unused)]
fn main() {
// 带有背压的有界 channel:
let (tx, rx) = mpsc::sync_channel(10); // 10 条消息的缓冲区
thread::spawn(move || {
for i in 0..1000 {
tx.send(i).unwrap(); // 如果缓冲区满则阻塞 —— 自然的背压
}
});
}
注意:
.unwrap()是为了简洁。在生产环境中,应该处理SendError(接收者被 drop)而不是 panic。
crossbeam-channel — 生产主力
crossbeam-channel 是生产环境中 channel 使用的实际标准。它比 std::sync::mpsc 更快,并支持多消费者(mpmc):
// Cargo.toml:
// [dependencies]
// crossbeam-channel = "0.5"
use crossbeam_channel::{bounded, unbounded, select, Sender, Receiver};
use std::thread;
use std::time::Duration;
fn main() {
// 有界 MPMC channel
let (tx, rx) = bounded::<String>(100);
// 多个生产者
for id in 0..4 {
let tx = tx.clone();
thread::spawn(move || {
for i in 0..10 {
tx.send(format!("worker-{id}: item-{i}")).unwrap();
}
});
}
drop(tx); // drop 原始发送者,让 channel 可以关闭
// 多个消费者(std::sync::mpsc 不支持!)
let rx2 = rx.clone();
let consumer1 = thread::spawn(move || {
while let Ok(msg) = rx.recv() {
println!("[consumer-1] {msg}");
}
});
let consumer2 = thread::spawn(move || {
while let Ok(msg) = rx2.recv() {
println!("[consumer-2] {msg}");
}
});
consumer1.join().unwrap();
consumer2.join().unwrap();
}
Channel 选择 (select!)
同时监听多个 channel —— 就像 Go 中的 select:
use crossbeam_channel::{bounded, tick, after, select};
use std::time::Duration;
fn main() {
let (work_tx, work_rx) = bounded::<String>(10);
let ticker = tick(Duration::from_secs(1)); // 周期性 tick
let deadline = after(Duration::from_secs(10)); // 单次超时
// 生产者
let tx = work_tx.clone();
std::thread::spawn(move || {
for i in 0..100 {
tx.send(format!("job-{i}")).unwrap();
std::thread::sleep(Duration::from_millis(500));
}
});
drop(work_tx);
loop {
select! {
recv(work_rx) -> msg => {
match msg {
Ok(job) => println!("Processing: {job}"),
Err(_) => {
println!("Work channel closed");
break;
}
}
},
recv(ticker) -> _ => {
println!("Tick — heartbeat");
},
recv(deadline) -> _ => {
println!("Deadline reached — shutting down");
break;
},
}
}
}
Go 对比: 这完全像 Go 的
select语句用于 channel。 crossbeam 的select!宏随机化顺序以防止饥饿,就像 Go 一样。
有界 vs 无界和背压
| 类型 | 满时的行为 | 内存 | 使用场景 |
|---|---|---|---|
| 无界 | 从不阻塞(堆增长) | 无界 ⚠️ | 罕见 —— 仅当生产者比消费者慢时 |
| 有界 | send() 阻塞直到有空闲 | 固定 | 生产默认 —— 防止 OOM |
| Rendezvous (bounded(0)) | send() 阻塞直到接收者准备好 | 无 | 同步/交接 |
#![allow(unused)]
fn main() {
// Rendezvous channel —— 零容量,直接交接
let (tx, rx) = crossbeam_channel::bounded(0);
// tx.send(x) 阻塞直到 rx.recv() 被调用,反之亦然。
// 这精确地同步了两个线程。
}
规则:在生产环境中总是使用有界 channel,除非你能证明生产者永远不会超过消费者的速度。
使用 Channel 的 Actor 模式
actor 模式使用 channel 来序列化对可变状态的访问 —— 不需要 mutex:
use std::sync::mpsc;
use std::thread;
// actor 可以接收的消息
enum CounterMsg {
Increment,
Decrement,
Get(mpsc::Sender<i64>), // 回复 channel
}
struct CounterActor {
count: i64,
rx: mpsc::Receiver<CounterMsg>,
}
impl CounterActor {
fn new(rx: mpsc::Receiver<CounterMsg>) -> Self {
CounterActor { count: 0, rx }
}
fn run(mut self) {
while let Ok(msg) = self.rx.recv() {
match msg {
CounterMsg::Increment => self.count += 1,
CounterMsg::Decrement => self.count -= 1,
CounterMsg::Get(reply) => {
let _ = reply.send(self.count);
}
}
}
}
}
// Actor handle —— 克隆成本低,Send + Sync
#[derive(Clone)]
struct Counter {
tx: mpsc::Sender<CounterMsg>,
}
impl Counter {
fn spawn() -> Self {
let (tx, rx) = mpsc::channel();
thread::spawn(move || CounterActor::new(rx).run());
Counter { tx }
}
fn increment(&self) { let _ = self.tx.send(CounterMsg::Increment); }
fn decrement(&self) { let _ = self.tx.send(CounterMsg::Decrement); }
fn get(&self) -> i64 {
let (reply_tx, reply_rx) = mpsc::channel();
self.tx.send(CounterMsg::Get(reply_tx)).unwrap();
reply_rx.recv().unwrap()
}
}
fn main() {
let counter = Counter::spawn();
// 多个线程可以安全地使用 counter —— 不需要 mutex!
let handles: Vec<_> = (0..10).map(|_| {
let counter = counter.clone();
thread::spawn(move || {
for _ in 0..1000 {
counter.increment();
}
})
}).collect();
for h in handles { h.join().unwrap(); }
println!("Final count: {}", counter.get()); // 10000
}
何时使用 actor vs mutex:当状态有复杂的不变量、操作需要很长时间,或者你想要序列化访问而不必考虑锁顺序时,actor 很好。Mutex 对于短的临界区更简单。
关键要点 —— Channels
crossbeam-channel是生产主力 —— 比std::sync::mpsc更快、功能更丰富select!用声明式的 channel 选择替代复杂的多源轮询- 有界 channel 提供自然的背压;无界 channel 有 OOM 风险
另见: 第 6 章 — 并发 了解线程、Mutex 和共享状态。第 16 章 — Async 了解 async channel(
tokio::sync::mpsc)。
练习:基于 Channel 的工作池 ★★★(约 45 分钟)
构建一个使用 channel 的工作池,其中:
- 调度器通过 channel 发送
Job结构体 - N 个工作线程消费作业并发送结果回来
- 使用
std::sync::mpsc和Arc<Mutex<Receiver>>实现共享工作队列
🔑 解答
use std::sync::mpsc;
use std::thread;
struct Job {
id: u64,
data: String,
}
struct JobResult {
job_id: u64,
output: String,
worker_id: usize,
}
fn worker_pool(jobs: Vec<Job>, num_workers: usize) -> Vec<JobResult> {
let (job_tx, job_rx) = mpsc::channel::<Job>();
let (result_tx, result_rx) = mpsc::channel::<JobResult>();
let job_rx = std::sync::Arc::new(std::sync::Mutex::new(job_rx));
let mut handles = Vec::new();
for worker_id in 0..num_workers {
let job_rx = job_rx.clone();
let result_tx = result_tx.clone();
handles.push(thread::spawn(move || {
loop {
let job = {
let rx = job_rx.lock().unwrap();
rx.recv()
};
match job {
Ok(job) => {
let output = format!("processed '{}' by worker {worker_id}", job.data);
result_tx.send(JobResult {
job_id: job.id, output, worker_id,
}).unwrap();
}
Err(_) => break,
}
}
}));
}
drop(result_tx);
let num_jobs = jobs.len();
for job in jobs {
job_tx.send(job).unwrap();
}
drop(job_tx);
let results: Vec<_> = result_rx.into_iter().collect();
assert_eq!(results.len(), num_jobs);
for h in handles { h.join().unwrap(); }
results
}
fn main() {
let jobs: Vec<Job> = (0..20).map(|i| Job {
id: i, data: format!("task-{i}"),
}).collect();
let results = worker_pool(jobs, 4);
for r in &results {
println!("[worker {}] job {}: {}", r.worker_id, r.job_id, r.output);
}
}
6. Concurrency vs Parallelism vs Threads 🟡
你将学到:
- 并发和并行之间的精确区别
- OS 线程、scoped threads 和用于数据并行的 rayon
- 共享状态原语:Arc、Mutex、RwLock、Atomics、Condvar
- 使用 OnceLock/LazyLock 进行惰性初始化以及无锁模式
术语:并发 ≠ 并行
这些术语经常混淆。这里是精确的区别:
| 并发 (Concurrency) | 并行 (Parallelism) | |
|---|---|---|
| 定义 | 管理多个可以进展的任务 | 同时执行多个任务 |
| 硬件要求 | 一个核心就够 | 需要多个核心 |
| 类比 | 一个厨师,多道菜(在它们之间切换) | 多个厨师,每人做一道菜 |
| Rust 工具 | async/await、channels、select! | rayon、thread::spawn、par_iter() |
并发(单核心): 并行(多核心):
Task A: ██░░██░░██ Task A: ██████████
Task B: ░░██░░██░░ Task B: ██████████
─────────────────→ time ─────────────────→ time
(在一个核心上交错) (在两个核心上同时)
std::thread — OS 线程
Rust 线程 1:1 映射到 OS 线程。每个线程有自己的栈(通常 2-8 MB):
use std::thread;
use std::time::Duration;
fn main() {
// 生成一个线程 —— 接受一个闭包
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread: {i}");
thread::sleep(Duration::from_millis(100));
}
42 // 返回值
});
// 同时在主线程上工作
for i in 0..3 {
println!("main thread: {i}");
thread::sleep(Duration::from_millis(150));
}
// 等待线程完成并获取其返回值
let result = handle.join().unwrap(); // 如果线程 panic 则 unwrap panic
println!("Thread returned: {result}");
}
Thread::spawn 类型要求:
#![allow(unused)]
fn main() {
// 闭包必须是:
// 1. Send —— 可以转移到另一个线程
// 2. 'static —— 不能从调用作用域借用
// 3. FnOnce —— 获取捕获变量的所有权
let data = vec![1, 2, 3];
// ❌ 借用 data —— 不是 'static
// thread::spawn(|| println!("{data:?}"));
// ✅ 将所有权移入线程
thread::spawn(move || println!("{data:?}"));
// data 在这里不再可访问
}
Scoped Threads (std::thread::scope)
从 Rust 1.63 开始,scoped threads 解决了 'static 要求 —— 线程可以从父作用域借用:
use std::thread;
fn main() {
let mut data = vec![1, 2, 3, 4, 5];
thread::scope(|s| {
// 线程 1:借用共享引用
s.spawn(|| {
let sum: i32 = data.iter().sum();
println!("Sum: {sum}");
});
// 线程 2:也可以借用共享引用(多个读取者 OK)
s.spawn(|| {
let max = data.iter().max().unwrap();
println!("Max: {max}");
});
// ❌ 当共享借用存在时不能可变借用:
// s.spawn(|| data.push(6));
});
// 所有 scoped threads 在这里 join —— 保证在作用域返回前完成
// 现在可以安全可变 —— 所有线程已完成
data.push(6);
println!("Updated: {data:?}");
}
这很重要:在 scoped threads 之前,你必须
Arc::clone()一切来与线程共享。现在你可以直接借用,编译器证明所有线程在数据离开作用域前完成。
rayon — 数据并行
rayon 提供并行迭代器,自动在线程池间分配工作:
// Cargo.toml: rayon = "1"
use rayon::prelude::*;
fn main() {
let data: Vec<u64> = (0..1_000_000).collect();
// 顺序:
let sum_seq: u64 = data.iter().map(|x| x * x).sum();
// 并行 —— 只需将 .iter() 改为 .par_iter():
let sum_par: u64 = data.par_iter().map(|x| x * x).sum();
assert_eq!(sum_seq, sum_par);
// 并行排序:
let mut numbers = vec![5, 2, 8, 1, 9, 3];
numbers.par_sort();
// 并行处理带 map/filter/collect:
let results: Vec<_> = data
.par_iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| expensive_computation(x))
.collect();
}
fn expensive_computation(x: u64) -> u64 {
// 模拟 CPU 重型工作
(0..1000).fold(x, |acc, _| acc.wrapping_mul(7).wrapping_add(13))
}
何时使用 rayon vs 线程:
| 使用 | 何时 |
|---|---|
rayon::par_iter() | 并行处理集合(map、filter、reduce) |
thread::spawn | 长运行后台任务、I/O 工作者 |
thread::scope | 借用本地数据的短生命周期并行任务 |
async + tokio | I/O 绑定并发(网络、文件 I/O) |
共享状态:Arc、Mutex、RwLock、Atomics
当线程需要共享可变状态时,Rust 提供安全的抽象:
注意: 这些示例中的
.lock()、.read()和.write()上的.unwrap()是为了简洁。 这些调用仅在另一个线程在持有锁时 panic(“中毒”)时失败。生产代码应该决定是否 从中毒的锁中恢复或传播错误。
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex, RwLock};
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
// --- Arc<Mutex<T>>: 共享 + 独占访问 ---
fn mutex_example() {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
let mut guard = counter.lock().unwrap();
*guard += 1;
} // Guard drop → 锁释放
}));
}
for h in handles { h.join().unwrap(); }
println!("Counter: {}", counter.lock().unwrap()); // 10000
}
// --- Arc<RwLock<T>>: 多个读取者 OR 一个写入者 ---
fn rwlock_example() {
let config = Arc::new(RwLock::new(String::from("initial")));
// 多个读取者 —— 不互相阻塞
let readers: Vec<_> = (0..5).map(|id| {
let config = Arc::clone(&config);
thread::spawn(move || {
let guard = config.read().unwrap();
println!("Reader {id}: {guard}");
})
}).collect();
// 写入者 —— 阻塞并等待所有读取者完成
{
let mut guard = config.write().unwrap();
*guard = "updated".to_string();
}
for r in readers { r.join().unwrap(); }
}
// --- Atomics: 简单值的无锁操作 ---
fn atomic_example() {
let counter = Arc::new(AtomicU64::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::Relaxed);
// 无锁、无 mutex —— 硬件原子指令
}
}));
}
for h in handles { h.join().unwrap(); }
println!("Atomic counter: {}", counter.load(Ordering::Relaxed)); // 10000
}
}
快速对比
| 原语 | 使用场景 | 成本 | 竞争 |
|---|---|---|---|
Mutex<T> | 短临界区 | 锁 + 解锁 | 线程排队等待 |
RwLock<T> | 读多写少 | 读写锁 | 读取者并发,写入者独占 |
AtomicU64 等 | 计数器、标志 | 硬件 CAS | 无锁 —— 无等待 |
| Channels | 消息传递 | 队列操作 | 生产者/消费者解耦 |
条件变量 (Condvar)
Condvar 允许线程等待,直到另一个线程发出信号表明条件为真,无需忙循环。
它总是与 Mutex 配对使用:
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair2 = Arc::clone(&pair);
// 生成的线程:等待直到 ready == true
let handle = thread::spawn(move || {
let (lock, cvar) = &*pair2;
let mut ready = lock.lock().unwrap();
while !*ready {
ready = cvar.wait(ready).unwrap(); // 原子地解锁 + 睡眠
}
println!("Worker: condition met, proceeding");
});
// 主线程:设置 ready = true,然后发信号
{
let (lock, cvar) = &*pair;
let mut ready = lock.lock().unwrap();
*ready = true;
cvar.notify_one(); // 唤醒一个等待线程(多个用 notify_all)
}
handle.join().unwrap();
}
模式:在
wait()返回后,总是在while循环中重新检查条件 —— OS 允许虚假唤醒。
惰性初始化:OnceLock 和 LazyLock
在 Rust 1.80 之前,初始化需要运行时计算的的全局 static(例如,解析配置、编译正则表达式)
需要 lazy_static! 宏或 once_cell crate。标准库现在提供两种类型来原生覆盖这些使用场景:
#![allow(unused)]
fn main() {
use std::sync::{OnceLock, LazyLock};
use std::collections::HashMap;
// OnceLock —— 通过 `get_or_init` 在首次使用时初始化。
// 适用于初始化值依赖于运行时参数的情况。
static CONFIG: OnceLock<HashMap<String, String>> = OnceLock::new();
fn get_config() -> &'static HashMap<String, String> {
CONFIG.get_or_init(|| {
// 开销大:读取和解析配置文件 —— 恰好发生一次。
let mut m = HashMap::new();
m.insert("log_level".into(), "info".into());
m
})
}
// LazyLock —— 在首次访问时初始化,闭包在定义位置提供。
// 等价于 lazy_static! 但无需宏。
static REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"^[a-zA-Z0-9_]+$").unwrap()
});
fn is_valid_identifier(s: &str) -> bool {
REGEX.is_match(s) // 首次调用编译正则表达式;后续调用重用。
}
}
| 类型 | 稳定版本 | 初始化时机 | 何时使用 |
|---|---|---|---|
OnceLock<T> | Rust 1.70 | 调用位置 (get_or_init) | 初始化依赖于运行时参数 |
LazyLock<T> | Rust 1.80 | 定义位置(闭包) | 初始化是自包含的 |
lazy_static! | — | 定义位置(宏) | 1.80 之前的代码库(建议迁移) |
const fn + static | 始终 | 编译时 | 值可在编译时计算 |
迁移提示:将
lazy_static! { static ref X: T = expr; }替换为static X: LazyLock<T> = LazyLock::new(|| expr);—— 相同的语义,无需宏, 无需外部依赖。
无锁模式
对于高性能代码,完全避免锁:
#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
// 模式 1:自旋锁(教育用途 —— 推荐 std::sync::Mutex)
// ⚠️ 警告:这只是教学示例。真实的自旋锁需要:
// - RAII guard(防止持有期间 panic 导致永久死锁)
// - 公平性保证(此例在竞争下会饥饿)
// - 回退策略(指数回退,向 OS 让出)
// 在生产中使用 std::sync::Mutex 或 parking_lot::Mutex。
struct SpinLock {
locked: AtomicBool,
}
impl SpinLock {
fn new() -> Self { SpinLock { locked: AtomicBool::new(false) } }
fn lock(&self) {
while self.locked
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
std::hint::spin_loop(); // CPU 提示:我们正在自旋
}
}
fn unlock(&self) {
self.locked.store(false, Ordering::Release);
}
}
// 模式 2:无锁 SPSC(单生产者,单消费者)
// 在生产中使用 crossbeam::queue::ArrayQueue 或类似
// 自己实现仅用于学习。
// 模式 3:用于无等待读取的序列计数器
// ⚠️ 最适合单机器字类型(u64、f64);更宽的 T 可能在读取时撕裂。
struct SeqLock<T: Copy> {
seq: AtomicUsize,
data: std::cell::UnsafeCell<T>,
}
unsafe impl<T: Copy + Send> Sync for SeqLock<T> {}
impl<T: Copy> SeqLock<T> {
fn new(val: T) -> Self {
SeqLock {
seq: AtomicUsize::new(0),
data: std::cell::UnsafeCell::new(val),
}
}
fn read(&self) -> T {
loop {
let s1 = self.seq.load(Ordering::Acquire);
if s1 & 1 != 0 { continue; } // 写入进行中,重试
// 安全性:我们使用 ptr::read_volatile 防止编译器重新排序或缓存读取。
// SeqLock 协议(在读取后检查 s1 == s2)确保如果有写入者活跃我们会重试。
// 这类似于 C SeqLock 模式,其中数据读取必须使用 volatile/relaxed 语义
// 以避免并发下的撕裂。
let value = unsafe { core::ptr::read_volatile(self.data.get() as *const T) };
// Acquire fence:确保上面的数据读取在我们重新检查序列计数器之前有序。
std::sync::atomic::fence(Ordering::Acquire);
let s2 = self.seq.load(Ordering::Relaxed);
if s1 == s2 { return value; } // 没有写入者介入
// 否则重试
}
}
/// # 安全性约定
/// 一次只能有一个线程调用 `write()`。如果需要多个写入者,
/// 在外部用 `Mutex` 包装 `write()` 调用。
fn write(&self, val: T) {
// 递增到奇数(表示写入进行中)。
// AcqRel:Acquire 端防止随后的数据写入在这个递增之前重新排序
// (读取者必须在观察到部分写入之前看到奇数)。Release 端
// 对于单个写入者 technically 不必要,但无害且一致。
self.seq.fetch_add(1, Ordering::AcqRel);
// 安全性:单一写入者不变量由调用者维持(见上面文档)。
// UnsafeCell 允许内部可变性;seq 计数器保护读取者。
unsafe { *self.data.get() = val; }
// 递增到偶数(表示写入完成)。
// Release:确保在读取者看到偶数 seq 之前数据写入可见。
self.seq.fetch_add(1, Ordering::Release);
}
}
}
⚠️ Rust 内存模型注意事项:
write()中通过UnsafeCell的非原子写入与read()中的非原子ptr::read_volatile在 Rust 抽象机下 technical 是数据竞争 —— 即使 SeqLock 协议确保读取者总是在陈旧数据上重试。这类似于 C kernel SeqLock 模式,并且在所有现代硬件上对适合单机器字的类型T(如u64)在实践中是可靠的。 对于更宽的类型,考虑对数据字段使用AtomicU64或将访问包装在Mutex中。 参见 Rust unsafe code guidelines 了解UnsafeCell并发的演进故事。
实用建议:无锁代码很难写对。使用
Mutex或RwLock,除非性能分析显示 锁竞争是瓶颈。当你确实需要无锁时,使用成熟的 crate(crossbeam、arc-swap、dashmap) 而不是自己实现。
关键要点 —— Concurrency
- Scoped threads(
thread::scope)让你借用栈数据而无需Arcrayon::par_iter()用一个方法调用并行化迭代器- 使用
OnceLock/LazyLock替代lazy_static!;在诉诸原子操作之前先用Mutex- 无锁代码很难 —— 优先使用成熟的 crate 而不是手写实现
另见: 第 5 章 — Channels 了解消息传递并发。第 9 章 — 智能指针 了解 Arc/Rc 详情。
flowchart TD
A["需要共享<br>可变状态?"] -->|是 | B{"多少<br>竞争?"}
A -->|否 | C["使用 channels<br>(第 5 章)"]
B -->|"读多"| D["RwLock"]
B -->|"短临界区"| E["Mutex"]
B -->|"简单计数器<br>或标志"| F["Atomics"]
B -->|"复杂状态"| G["Actor + channels"]
H["需要并行?"] -->|"集合<br>处理"| I["rayon::par_iter"]
H -->|"后台任务"| J["thread::spawn"]
H -->|"借用本地数据"| K["thread::scope"]
style A fill:#e8f4f8,stroke:#2980b9,color:#000
style B fill:#fef9e7,stroke:#f1c40f,color:#000
style C fill:#d4efdf,stroke:#27ae60,color:#000
style D fill:#fdebd0,stroke:#e67e22,color:#000
style E fill:#fdebd0,stroke:#e67e22,color:#000
style F fill:#fdebd0,stroke:#e67e22,color:#000
style G fill:#fdebd0,stroke:#e67e22,color:#000
style H fill:#e8f4f8,stroke:#2980b9,color:#000
style I fill:#d4efdf,stroke:#27ae60,color:#000
style J fill:#d4efdf,stroke:#27ae60,color:#000
style K fill:#d4efdf,stroke:#27ae60,color:#000
练习:使用 Scoped Threads 的并行 Map ★★(约 25 分钟)
编写函数 parallel_map<T, R>(data: &[T], f: fn(&T) -> R, num_threads: usize) -> Vec<R>,
将 data 分割成 num_threads 个块,并在 scoped thread 中处理每个块。不要使用 rayon —— 使用 std::thread::scope。
🔑 解答
fn parallel_map<T: Sync, R: Send>(data: &[T], f: fn(&T) -> R, num_threads: usize) -> Vec<R> {
let chunk_size = (data.len() + num_threads - 1) / num_threads;
let mut results = Vec::with_capacity(data.len());
std::thread::scope(|s| {
let mut handles = Vec::new();
for chunk in data.chunks(chunk_size) {
handles.push(s.spawn(move || {
chunk.iter().map(f).collect::<Vec<_>>()
}));
}
for h in handles {
results.extend(h.join().unwrap());
}
});
results
}
fn main() {
let data: Vec<u64> = (1..=20).collect();
let squares = parallel_map(&data, |x| x * x, 4);
assert_eq!(squares, (1..=20).map(|x: u64| x * x).collect::<Vec<_>>());
println!("Parallel squares: {squares:?}");
}
7. Closures and Higher-Order Functions 🟢
你将学到:
- 三个闭包 trait(
Fn、FnMut、FnOnce)以及捕获如何工作- 将闭包作为参数传递和从函数返回
- 用于函数式风格编程的组合器链和迭代器适配器
- 使用正确的 trait bound 设计你自己的高阶 API
Fn、FnMut、FnOnce — 闭包 Trait
Rust 中的每个闭包实现三个 trait 中的一个或多个,基于它如何捕获变量:
#![allow(unused)]
fn main() {
// FnOnce —— 消费捕获的值(只能调用一次)
let name = String::from("Alice");
let greet = move || {
println!("Hello, {name}!"); // 获取 `name` 的所有权
drop(name); // name 被消费
};
greet(); // ✅ 第一次调用
// greet(); // ❌ 不能再调用 —— `name` 被消费了
// FnMut —— 可变借用捕获的值(可以调用多次)
let mut count = 0;
let mut increment = || {
count += 1; // 可变借用 `count`
};
increment(); // count == 1
increment(); // count == 2
// Fn —— 不可变借用捕获的值(可以多次、并发调用)
let prefix = "Result";
let display = |x: i32| {
println!("{prefix}: {x}"); // 不可变借用 `prefix`
};
display(1);
display(2);
}
层次结构:Fn : FnMut : FnOnce —— 每个都是下一个的子 trait:
FnOnce ← 所有东西都可以至少调用一次
↑
FnMut ← 可以重复调用(可能改变状态)
↑
Fn ← 可以重复和并发调用(无可变性)
如果闭包实现 Fn,它也必须实现 FnMut 和 FnOnce。
闭包作为参数和返回值
// --- 参数 ---
// 静态 dispatch(单态化 —— 最快)
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
// 也用 impl Trait 编写:
fn apply_twice_v2(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// 动态 dispatch(trait object —— 灵活,轻微开销)
fn apply_dyn(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
// --- 返回值 ---
// 不能在没有 boxing 的情况下按值返回闭包(它们有匿名类型):
fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + n)
}
// 使用 impl Trait(更简单,单态化,但不能动态):
fn make_adder_v2(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let double = |x: i32| x * 2;
println!("{}", apply_twice(double, 3)); // 12
let add5 = make_adder(5);
println!("{}", add5(10)); // 15
}
组合器链和迭代器适配器
高阶函数在迭代器上发光 —— 这是习惯性的 Rust:
#![allow(unused)]
fn main() {
// C 风格循环(命令式):
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut result = Vec::new();
for x in &data {
if x % 2 == 0 {
result.push(x * x);
}
}
// 习惯 Rust(函数式组合器链):
let result: Vec<i32> = data.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
// 相同性能 —— 迭代器是惰性的并被 LLVM 优化
assert_eq!(result, vec![4, 16, 36, 64, 100]);
}
常见组合器速查表:
| 组合器 | 做什么 | 示例 |
|---|---|---|
.map(f) | 转换每个元素 | `.map( |
.filter(p) | 保留谓词为 true 的元素 | `.filter( |
.filter_map(f) | 一步完成 Map + filter(返回 Option) | `.filter_map( |
.flat_map(f) | Map 然后扁平化嵌套迭代器 | `.flat_map( |
.fold(init, f) | 归约为单个值(如 C# 中的 Aggregate) | `.fold(0, |
.any(p) / .all(p) | 短路布尔检查 | `.any( |
.enumerate() | 添加索引 | `.enumerate().map( |
.zip(other) | 与另一个迭代器配对 | .zip(labels.iter()) |
.take(n) / .skip(n) | 前 N 个/跳过 N 个元素 | .take(10) |
.chain(other) | 连接两个迭代器 | .chain(extra.iter()) |
.peekable() | 前瞻而不消费 | .peek() |
.collect() | 收集到集合中 | .collect::<Vec<_>>() |
实现你自己的高阶 API
设计接受闭包的 API 以实现自定义:
#![allow(unused)]
fn main() {
/// 使用可配置的策略重试操作
fn retry<T, E, F, S>(
mut operation: F,
mut should_retry: S,
max_attempts: usize,
) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
S: FnMut(&E, usize) -> bool, // (error, attempt) → 重试?
{
for attempt in 1..=max_attempts {
match operation() {
Ok(val) => return Ok(val),
Err(e) if attempt < max_attempts && should_retry(&e, attempt) => {
continue;
}
Err(e) => return Err(e),
}
}
unreachable!()
}
// 用法 —— 调用者控制重试逻辑:
}
#![allow(unused)]
fn main() {
fn connect_to_database() -> Result<(), String> { Ok(()) }
fn http_get(_url: &str) -> Result<String, String> { Ok(String::new()) }
trait TransientError { fn is_transient(&self) -> bool; }
impl TransientError for String { fn is_transient(&self) -> bool { true } }
let url = "http://example.com";
let result = retry(
|| connect_to_database(),
|err, attempt| {
eprintln!("Attempt {attempt} failed: {err}");
true // 总是重试
},
3,
);
// 用法 —— 仅重试特定错误:
let result = retry(
|| http_get(url),
|err, _| err.is_transient(), // 仅重试瞬时错误
5,
);
}
with 模式 —— 括号资源的访问
有时你需要保证资源在操作期间处于特定状态,并在之后恢复 —— 无论调用者的代码如何退出(提前返回、?、panic)。与其直接暴露资源并希望调用者记得设置和清理,通过闭包借用资源:
设置 → 用资源调用闭包 → 清理
调用者从不接触设置或清理。他们不会忘记、不会弄错,也不会在闭包作用域之外持有资源。
示例:GPIO Pin 方向
GPIO 控制器管理支持双向 I/O 的 pins。一些调用者需要 pin 配置为输入,另一些为输出。与其暴露原始 pin 访问并信任调用者正确设置方向,控制器提供 with_pin_input 和 with_pin_output:
/// GPIO pin 方向 —— 不公开,调用者从不直接设置这个。
#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction { In, Out }
/// 借给闭包的 GPIO pin handle。不能存储或克隆 ——
/// 它只存在于回调期间。
pub struct GpioPin<'a> {
pin_number: u8,
_controller: &'a GpioController,
}
impl GpioPin<'_> {
pub fn read(&self) -> bool {
// 从硬件寄存器读取 pin 电平
println!(" reading pin {}", self.pin_number);
true // stub
}
pub fn write(&self, high: bool) {
// 通过硬件寄存器驱动 pin 电平
println!(" writing pin {} = {high}", self.pin_number);
}
}
pub struct GpioController {
current_direction: std::cell::Cell<Option<Direction>>,
}
impl GpioController {
pub fn new() -> Self {
GpioController {
current_direction: std::cell::Cell::new(None),
}
}
/// 配置 pin 为输入,运行闭包,恢复状态。
/// 调用者接收一个 `GpioPin`,它只存在于回调期间。
pub fn with_pin_input<R>(
&self,
pin: u8,
mut f: impl FnMut(&GpioPin<'_>) -> R,
) -> R {
let prev = self.current_direction.get();
self.set_direction(pin, Direction::In);
let handle = GpioPin { pin_number: pin, _controller: self };
let result = f(&handle);
// 恢复之前的方向(或保持原样 —— 策略选择)
if let Some(dir) = prev {
self.set_direction(pin, dir);
}
result
}
/// 配置 pin 为输出,运行闭包,恢复状态。
pub fn with_pin_output<R>(
&self,
pin: u8,
mut f: impl FnMut(&GpioPin<'_>) -> R,
) -> R {
let prev = self.current_direction.get();
self.set_direction(pin, Direction::Out);
let handle = GpioPin { pin_number: pin, _controller: self };
let result = f(&handle);
if let Some(dir) = prev {
self.set_direction(pin, dir);
}
result
}
fn set_direction(&self, pin: u8, dir: Direction) {
println!(" [hw] pin {pin} → {dir:?}");
self.current_direction.set(Some(dir));
}
}
fn main() {
let gpio = GpioController::new();
// 调用者 1:需要输入 —— 不知道也不关心方向如何管理
let level = gpio.with_pin_input(4, |pin| {
pin.read()
});
println!("Pin 4 level: {level}");
// 调用者 2:需要输出 —— 相同的 API 形状,不同的保证
gpio.with_pin_output(4, |pin| {
pin.write(true);
// do more work...
pin.write(false);
});
// 不能在闭包之外使用 pin handle:
// let escaped_pin = gpio.with_pin_input(4, |pin| pin);
// ❌ ERROR: borrowed value does not live long enough
}
with 模式的保证:
- 方向总是在调用者代码运行之前设置
- 方向总是在之后恢复,即使闭包提前返回
GpioPinhandle不能逃逸闭包 —— 借用检查器通过与控制器引用绑定的生命周期强制执行- 调用者从不导入
Direction,从不调用set_direction—— API 无法误用
with 模式出现的位置
with 模式出现在 Rust 标准库和生态系统中:
| API | 设置 | 回调 | 清理 |
|---|---|---|---|
std::thread::scope | 创建作用域 | |s| { s.spawn(...) } | Join 所有线程 |
Mutex::lock | 获取锁 | 使用 MutexGuard(RAII,不是闭包,但相同思想) | drop 时释放 |
tempfile::tempdir | 创建临时目录 | 使用路径 | drop 时删除 |
std::io::BufWriter::new | 缓冲写入 | 写入操作 | drop 时刷新 |
GPIO with_pin_*(上面) | 设置方向 | 使用 pin handle | 恢复方向 |
基于闭包的变体在以下情况最强:
- 设置和清理是配对的,忘记其中任何一个都是 bug
- 资源不应该在操作之外存活 —— 借用检查器自然地强制执行
- 存在多种配置(
with_pin_inputvswith_pin_output) —— 每个with_*方法封装不同的设置,不向调用者暴露配置
withvs RAII (Drop): 两者都保证清理。当调用者需要在多个语句和函数调用中持有资源时使用 RAII /Drop。当操作是括号的 —— 一次设置、一块工作、一次清理 —— 并且你不希望调用者能够破坏括号时使用with。
API 设计中的 FnMut vs Fn:默认使用
FnMutbound —— 它最灵活(调用者可以传递Fn或FnMut闭包)。仅当你需要并发调用闭包(例如,从多个线程)时才要求Fn。仅当你恰好调用一次时才要求FnOnce。
关键要点 —— Closures
Fn借用,FnMut可变借用,FnOnce消费 —— 接受你的 API 需要的最弱的 bound- 参数中用
impl Fn,存储用Box<dyn Fn>,返回用impl Fn(或动态用Box<dyn Fn>)- 组合器链(
map、filter、and_then)组合清晰并内联到紧凑循环with模式(通过闭包的括号访问)保证设置/清理并防止资源逃逸 —— 当调用者不应该管理配置生命周期时使用它
另见: 第 2 章 — Traits 深入 了解
Fn/FnMut/FnOnce如何与 trait objects 关联。第 8 章 — 函数式 vs 命令式 了解何时选择组合器而非循环。第 15 章 — API 设计 了解 ergonomic 参数模式。
graph TD
FnOnce["FnOnce<br>(可以调用一次)"]
FnMut["FnMut<br>(可以调用多次,<br>可以可变捕获)"]
Fn["Fn<br>(可以调用多次,<br>不可变捕获)"]
Fn -->|"implements"| FnMut
FnMut -->|"implements"| FnOnce
style Fn fill:#d4efdf,stroke:#27ae60,color:#000
style FnMut fill:#fef9e7,stroke:#f1c40f,color:#000
style FnOnce fill:#fadbd8,stroke:#e74c3c,color:#000
每个
Fn也是FnMut,每个FnMut也是FnOnce。默认接受FnMut—— 它是对调用者最灵活的 bound。
练习:高阶组合器管道 ★★(约 25 分钟)
创建一个 Pipeline 结构体来链接转换。它应该支持 .pipe(f) 添加转换和 .execute(input) 运行完整链。
🔑 解答
struct Pipeline<T> {
transforms: Vec<Box<dyn Fn(T) -> T>>,
}
impl<T: 'static> Pipeline<T> {
fn new() -> Self {
Pipeline { transforms: Vec::new() }
}
fn pipe(mut self, f: impl Fn(T) -> T + 'static) -> Self {
self.transforms.push(Box::new(f));
self
}
fn execute(self, input: T) -> T {
self.transforms.into_iter().fold(input, |val, f| f(val))
}
}
fn main() {
let result = Pipeline::new()
.pipe(|s: String| s.trim().to_string())
.pipe(|s| s.to_uppercase())
.pipe(|s| format!(">>> {s} <<<"))
.execute(" hello world ".to_string());
println!("{result}"); // >>> HELLO WORLD <<<
let result = Pipeline::new()
.pipe(|x: i32| x * 2)
.pipe(|x| x + 10)
.pipe(|x| x * x)
.execute(5);
println!("{result}"); // (5*2 + 10)^2 = 400
}
8. Functional vs. Imperative: When Elegance Wins (and When It Doesn’t)
难度: 🟡 中级 | 时间: 2-3 小时 | 前置要求: 第 7 章 — 闭包
Rust 在函数式和命令式风格之间提供真正的对等。不像 Haskell(强制函数式)或 C(默认命令式),Rust 让你选择 —— 正确的选择取决于你要表达什么。本章培养正确判断的能力。
核心原则: 当你通过管道转换数据时函数式风格发光。当你管理带有副作用的状态转换时命令式风格发光。大多数真实代码两者都有,技能在于知道边界落在哪里。
8.1 你不知道你需要的组合器
许多 Rust 开发者这样写:
#![allow(unused)]
fn main() {
let value = if let Some(x) = maybe_config() {
x
} else {
default_config()
};
process(value);
}
当他们可以这样写:
#![allow(unused)]
fn main() {
process(maybe_config().unwrap_or_else(default_config));
}
或这个常见模式:
#![allow(unused)]
fn main() {
let display_name = if let Some(name) = user.nickname() {
name.to_uppercase()
} else {
"ANONYMOUS".to_string()
};
}
也就是:
#![allow(unused)]
fn main() {
let display_name = user.nickname()
.map(|n| n.to_uppercase())
.unwrap_or_else(|| "ANONYMOUS".to_string());
}
函数式版本不仅更短 —— 它告诉你什么在发生(转换,然后默认),而不必追踪控制流。if let 版本让你阅读分支来找出两条路径最终到达同一地方。
Option 组合器家族
这里是心理模型:Option<T> 是一个元素或空的集合。Option 上的每个组合器都有集合操作的类比。
| 你写… | 替代… | 传达什么 |
|---|---|---|
opt.unwrap_or(default) | if let Some(x) = opt { x } else { default } | “使用此值或回退” |
opt.unwrap_or_else(|| expensive()) | if let Some(x) = opt { x } else { expensive() } | 相同,但默认是惰性的 |
opt.map(f) | match opt { Some(x) => Some(f(x)), None => None } | “转换内部,传播缺失” |
opt.and_then(f) | match opt { Some(x) => f(x), None => None } | “链接可能失败的操作” (flatmap) |
opt.filter(|x| pred(x)) | match opt { Some(x) if pred(&x) => Some(x), _ => None } | “仅当通过时保留” |
opt.zip(other) | if let (Some(a), Some(b)) = (opt, other) { Some((a,b)) } else { None } | “两者或都不” |
opt.or(fallback) | if opt.is_some() { opt } else { fallback } | “第一个可用的” |
opt.or_else(|| try_another()) | if opt.is_some() { opt } else { try_another() } | “按顺序尝试” |
opt.map_or(default, f) | if let Some(x) = opt { f(x) } else { default } | “转换或默认” —— 一行 |
opt.map_or_else(default_fn, f) | if let Some(x) = opt { f(x) } else { default_fn() } | 相同,两边都是闭包 |
opt? | match opt { Some(x) => x, None => return None } | “向上传播缺失” |
Result 组合器家族
相同模式适用于 Result<T, E>:
| 你写… | 替代… | 传达什么 |
|---|---|---|
res.map(f) | match res { Ok(x) => Ok(f(x)), Err(e) => Err(e) } | 转换成功路径 |
res.map_err(f) | match res { Ok(x) => Ok(x), Err(e) => Err(f(e)) } | 转换错误 |
res.and_then(f) | match res { Ok(x) => f(x), Err(e) => Err(e) } | 链接可能失败的操作 |
res.unwrap_or_else(|e| default(e)) | match res { Ok(x) => x, Err(e) => default(e) } | 从错误恢复 |
res.ok() | match res { Ok(x) => Some(x), Err(_) => None } | “我不关心错误” |
res? | match res { Ok(x) => x, Err(e) => return Err(e.into()) } | 向上传播错误 |
何时 if let 更好
组合器在以下情况失败:
- 你需要在
Some分支中多条语句。 5 行的 map 闭包比 5 行的if let更糟。 - 控制流是重点。
if let Some(connection) = pool.try_get() { /* 使用它 */ } else { /* 日志、重试、警报 */ }—— 两个分支是真正不同的代码路径,不是转换或默认。 - 副作用主导。 如果两个分支都做 I/O 且有不同的错误处理,组合器版本模糊了重要的差异。
经验法则: 如果 else 分支产生与 Some 分支相同的类型且主体是短表达式,使用组合器。如果分支做 fundamentally 不同的事情,使用 if let 或 match。
8.2 Bool 组合器:.then() 和 .then_some()
另一个比应该更常见的模式:
#![allow(unused)]
fn main() {
let label = if is_admin {
Some("ADMIN")
} else {
None
};
}
Rust 1.62+ 给你:
#![allow(unused)]
fn main() {
let label = is_admin.then_some("ADMIN");
}
或带计算值:
#![allow(unused)]
fn main() {
let permissions = is_admin.then(|| compute_admin_permissions());
}
这在链中特别强大:
#![allow(unused)]
fn main() {
// 命令式
let mut tags = Vec::new();
if user.is_admin { tags.push("admin"); }
if user.is_verified { tags.push("verified"); }
if user.score > 100 { tags.push("power-user"); }
// 函数式
let tags: Vec<&str> = [
user.is_admin.then_some("admin"),
user.is_verified.then_some("verified"),
(user.score > 100).then_some("power-user"),
]
.into_iter()
.flatten()
.collect();
}
函数式版本使模式明确:“从条件元素构建列表。“命令式版本让你阅读每个 if 来确认它们都做相同的事情(push 标签)。
8.3 迭代器链 vs 循环:决策框架
第 7 章展示了机制。本节培养判断力。
何时迭代器赢
数据管道 —— 通过一系列步骤转换集合:
#![allow(unused)]
fn main() {
// 命令式:8 行,2 个可变变量
let mut results = Vec::new();
for item in inventory {
if item.category == Category::Server {
if let Some(temp) = item.last_temperature() {
if temp > 80.0 {
results.push((item.id, temp));
}
}
}
}
// 函数式:6 行,0 个可变变量,一个管道
let results: Vec<_> = inventory.iter()
.filter(|item| item.category == Category::Server)
.filter_map(|item| item.last_temperature().map(|t| (item.id, t)))
.filter(|(_, temp)| *temp > 80.0)
.collect();
}
函数式版本赢因为:
- 每个 filter 独立可读
- 无
mut—— 数据单向流动 - 你可以添加/删除/重新排序管道阶段而无需重构
- LLVM 内联迭代器适配器到与循环相同的机器码
聚合 —— 从集合计算单个值:
#![allow(unused)]
fn main() {
// 命令式
let mut total_power = 0.0;
let mut count = 0;
for server in fleet {
total_power += server.power_draw();
count += 1;
}
let avg = total_power / count as f64;
// 函数式
let (total_power, count) = fleet.iter()
.map(|s| s.power_draw())
.fold((0.0, 0usize), |(sum, n), p| (sum + p, n + 1));
let avg = total_power / count as f64;
}
或如果你只需要总和更简单:
#![allow(unused)]
fn main() {
let total: f64 = fleet.iter().map(|s| s.power_draw()).sum();
}
何时循环赢
提前退出带复杂状态:
#![allow(unused)]
fn main() {
// 这清晰且直接
let mut best_candidate = None;
for server in fleet {
let score = evaluate(server);
if score > threshold {
if server.is_available() {
best_candidate = Some(server);
break; // 找到一个 —— 立即停止
}
}
}
// 函数式版本是紧张的
let best_candidate = fleet.iter()
.filter(|s| evaluate(s) > threshold)
.find(|s| s.is_available());
}
等等 —— 那个函数式版本实际上相当干净。让我们尝试一个它真正失败的情况:
同时构建多个输出:
#![allow(unused)]
fn main() {
// 命令式:清晰,每个分支做不同的事情
let mut warnings = Vec::new();
let mut errors = Vec::new();
let mut stats = Stats::default();
for event in log_stream {
match event.severity {
Severity::Warn => {
warnings.push(event.clone());
stats.warn_count += 1;
}
Severity::Error => {
errors.push(event.clone());
stats.error_count += 1;
if event.is_critical() {
alert_oncall(&event);
}
}
_ => stats.other_count += 1,
}
}
// 函数式版本:强制、笨拙,没人想读这个
let (warnings, errors, stats) = log_stream.iter().fold(
(Vec::new(), Vec::new(), Stats::default()),
|(mut w, mut e, mut s), event| {
match event.severity {
Severity::Warn => { w.push(event.clone()); s.warn_count += 1; }
Severity::Error => {
e.push(event.clone()); s.error_count += 1;
if event.is_critical() { alert_oncall(event); }
}
_ => s.other_count += 1,
}
(w, e, s)
},
);
}
fold 版本更长、更难读,并且有可变性(mut 解构累加器)。循环赢因为:
- 多个输出同时构建
- 副作用(警报)混合到逻辑中
- 分支体是语句,不是表达式
带 I/O 的状态机:
#![allow(unused)]
fn main() {
// 读取 token 的解析器 —— 循环就是算法
let mut state = ParseState::Start;
loop {
let token = lexer.next_token()?;
state = match state {
ParseState::Start => match token {
Token::Keyword(k) => ParseState::GotKeyword(k),
Token::Eof => break,
_ => return Err(ParseError::UnexpectedToken(token)),
},
ParseState::GotKeyword(k) => match token {
Token::Ident(name) => ParseState::GotName(k, name),
_ => return Err(ParseError::ExpectedIdentifier),
},
// ...更多状态
};
}
}
没有函数式等价物更干净。带 match state 的循环是状态机的自然表达。
决策流程图
flowchart TB
START{你在做什么?}
START -->|"转换集合<br/>到另一个集合"| PIPE[使用迭代器链]
START -->|"从集合计算<br/>单个值"| AGG{多复杂?}
START -->|"从一次传递<br/>多个输出"| LOOP[使用 for 循环]
START -->|"带 I/O 或<br/>副作用的状态机"| LOOP
START -->|"一个 Option/Result<br/>转换 + 默认"| COMB[使用组合器]
AGG -->|"总和、计数、最小、最大"| BUILTIN["使用 .sum()、.count()、<br/>.min()、.max()"]
AGG -->|"自定义归约"| FOLD{累加器有可变性<br/>或副作用?}
FOLD -->|"否"| FOLDF["使用 .fold()"]
FOLD -->|"是"| LOOP
style PIPE fill:#d4efdf,stroke:#27ae60,color:#000
style COMB fill:#d4efdf,stroke:#27ae60,color:#000
style BUILTIN fill:#d4efdf,stroke:#27ae60,color:#000
style FOLDF fill:#d4efdf,stroke:#27ae60,color:#000
style LOOP fill:#fef9e7,stroke:#f1c40f,color:#000
侧边栏:Scoped 可变性 —— 内部命令式,外部函数式
Rust 块是表达式。这让你将可变性限制在构建阶段并将结果不可变地绑定:
#![allow(unused)]
fn main() {
use rand::random;
let samples = {
let mut buf = Vec::with_capacity(10);
while buf.len() < 10 {
let reading: f64 = random();
buf.push(reading);
if random::<u8>() % 3 == 0 { break; } // 随机提前停止
}
buf
};
// samples 是不可变的 —— 包含 1 到 10 个元素
}
内部 buf 仅在块内可变。一旦块产生,外部绑定 samples 是不可变的,编译器将拒绝任何后续的 samples.push(...)。
为什么不是迭代器链? 你可能尝试:
#![allow(unused)]
fn main() {
let samples: Vec<f64> = std::iter::from_fn(|| Some(random()))
.take(10)
.take_while(|_| random::<u8>() % 3 != 0)
.collect();
}
但 take_while 排除 失败谓词的元素,产生 0 到 9 个元素而不是命令式版本提供的至少一个保证。你可以用 scan 或 chain 解决,但命令式版本更清晰。
scoped 可变性真正赢的情况:
| 场景 | 为什么迭代器挣扎 |
|---|---|
排序然后冻结 (sort_unstable() + dedup()) | 两者都返回 () —— 无可链接输出(如果可用 itertools 提供 .sorted().dedup()) |
| 有状态的终止(停止在与数据无关的条件) | take_while drop 边界元素 |
| 多步骤结构体填充(从不同来源逐字段) | 无自然的单个管道 |
诚实的校准: 对于大多数集合构建任务,迭代器链或 itertools 是首选。当构建逻辑有分支、提前退出、或不映射到单个管道的原位可变性时使用 scoped 可变性。模式的真正价值是教导可变性作用域可以比变量生命周期更小 —— 一个 Rust 基础,让来自 C++、C# 和 Python 的开发者惊讶。
8.4 ? 操作符:函数式与命令式的融合
? 操作符是 Rust 两种风格最优雅的综合。它本质上是 .and_then() 与提前返回的结合:
#![allow(unused)]
fn main() {
// 这个 and_then 链...
fn load_config() -> Result<Config, Error> {
read_file("config.toml")
.and_then(|contents| parse_toml(&contents))
.and_then(|table| validate_config(table))
.and_then(|valid| Config::from_validated(valid))
}
// ...等价于这个
fn load_config() -> Result<Config, Error> {
let contents = read_file("config.toml")?;
let table = parse_toml(&contents)?;
let valid = validate_config(table)?;
Config::from_validated(valid)
}
}
两者在精神上都是函数式的(它们自动传播错误),但 ? 版本给你命名的中间变量,这在以下情况重要:
- 你需要稍后再次使用
contents - 你想每步添加
.context("while parsing config")? - 你在调试并想检查中间值
反模式: 当 ? 可用时长 .and_then() 链。如果链中的每个闭包都是 |x| next_step(x),你重新发明了 ? 而没有可读性。
何时 .and_then() 比 ? 好:
#![allow(unused)]
fn main() {
// 在 Option 内部转换,无提前返回
let port: Option<u16> = config.get("port")
.and_then(|v| v.parse::<u16>().ok())
.filter(|&p| p > 0 && p < 65535);
}
你不能在这里使用 ? 因为没有封闭函数可返回 —— 你在构建 Option,不是传播它。
8.5 集合构建:collect() vs Push 循环
collect() 比大多数开发者意识到的更强大:
收集到 Result
#![allow(unused)]
fn main() {
// 命令式:解析列表,在第一个错误失败
let mut numbers = Vec::new();
for s in input_strings {
let n: i64 = s.parse().map_err(|_| Error::BadInput(s.clone()))?;
numbers.push(n);
}
// 函数式:收集到 Result<Vec<_>, _>
let numbers: Vec<i64> = input_strings.iter()
.map(|s| s.parse::<i64>().map_err(|_| Error::BadInput(s.clone())))
.collect::<Result<_, _>>()?;
}
collect::<Result<Vec<_>, _>>() 技巧有效因为 Result 实现 FromIterator。它在第一个 Err 短路,就像带 ? 的循环。
收集到 HashMap
#![allow(unused)]
fn main() {
// 命令式
let mut index = HashMap::new();
for server in fleet {
index.insert(server.id.clone(), server);
}
// 函数式
let index: HashMap<_, _> = fleet.into_iter()
.map(|s| (s.id.clone(), s))
.collect();
}
收集到 String
#![allow(unused)]
fn main() {
// 命令式
let mut csv = String::new();
for (i, field) in fields.iter().enumerate() {
if i > 0 { csv.push(','); }
csv.push_str(field);
}
// 函数式
let csv = fields.join(",");
// 或对于更复杂格式化:
let csv: String = fields.iter()
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(",");
}
何时循环版本赢
collect() 分配新集合。如果你原位修改,循环更清晰和更高效:
#![allow(unused)]
fn main() {
// 原位更新 —— 无更好的函数式等价物
for server in &mut fleet {
if server.needs_refresh() {
server.refresh_telemetry()?;
}
}
}
函数式版本需要 .iter_mut().for_each(|s| { ... }),这只是带额外语法的循环。
8.6 模式匹配作为函数分发
Rust 的 match 是大多数开发者命令式使用的函数式结构。这里是函数式视角:
Match 作为查找表
#![allow(unused)]
fn main() {
// 命令式思维:"检查每个情况"
fn status_message(code: StatusCode) -> &'static str {
if code == StatusCode::OK { "Success" }
else if code == StatusCode::NOT_FOUND { "Not found" }
else if code == StatusCode::INTERNAL { "Server error" }
else { "Unknown" }
}
// 函数式思维:"从定义域映射到值域"
fn status_message(code: StatusCode) -> &'static str {
match code {
StatusCode::OK => "Success",
StatusCode::NOT_FOUND => "Not found",
StatusCode::INTERNAL => "Server error",
_ => "Unknown",
}
}
}
match 版本不仅是风格 —— 编译器验证穷尽性。添加新变体,每个不处理它的 match 变成编译错误。if/else 链静默落入默认。
Match + 解构作为管道
#![allow(unused)]
fn main() {
// 解析命令 —— 每个 arm 提取和转换
fn execute(cmd: Command) -> Result<Response, Error> {
match cmd {
Command::Get { key } => db.get(&key).map(Response::Value),
Command::Set { key, value } => db.set(key, value).map(|_| Response::Ok),
Command::Delete { key } => db.delete(&key).map(|_| Response::Ok),
Command::Batch(cmds) => cmds.into_iter()
.map(execute)
.collect::<Result<Vec<_>, _>>()
.map(Response::Batch),
}
}
}
每个 arm 是返回相同类型的表达式。这是作为函数分发的模式匹配 —— match arm 本质上是按枚举变体索引的函数表。
8.7 在自定义类型上链接方法
函数式风格扩展到标准库类型之外。Builder 模式和流畅 APIs 是伪装的函数式编程:
#![allow(unused)]
fn main() {
// 这是在你自己类型上的组合器链
let query = QueryBuilder::new("servers")
.filter("status", Eq, "active")
.filter("rack", In, &["A1", "A2", "B1"])
.order_by("temperature", Desc)
.limit(50)
.build();
}
关键洞察: 如果你的类型有接受 self 并返回 Self(或转换类型)的方法,你构建了组合器。相同的函数式/命令式判断适用:
#![allow(unused)]
fn main() {
// 好:可链接因为每步是简单转换
let config = Config::default()
.with_timeout(Duration::from_secs(30))
.with_retries(3)
.with_tls(true);
// 坏:可链接但链在做太多不相关的事情
let result = processor
.load_data(path)? // I/O
.validate() // 纯
.transform(rule_set) // 纯
.save_to_disk(output)? // I/O
.notify_downstream()?; // 副作用
// 更好:分离纯管道和 I/O 书端
let data = load_data(path)?;
let processed = data.validate().transform(rule_set);
save_to_disk(output, &processed)?;
notify_downstream()?;
}
当链混合纯转换与 I/O 时失败。读者不能分辨哪些调用可能失败、哪些有副作用、实际数据转换发生在哪里。
8.8 性能:它们相同
常见误解:“函数式风格更慢因为所有闭包和分配。”
在 Rust 中,迭代器链编译到与手写循环相同的机器码。 LLVM 内联闭包调用,消除迭代器适配器结构体,并且经常产生相同的汇编。这叫零成本抽象,它不是愿望 —— 它是测量的。
#![allow(unused)]
fn main() {
// 这些在 release 构建中产生相同的汇编:
// 函数式
let sum: i64 = (0..1000).filter(|n| n % 2 == 0).map(|n| n * n).sum();
// 命令式
let mut sum: i64 = 0;
for n in 0..1000 {
if n % 2 == 0 {
sum += n * n;
}
}
}
唯一的例外: .collect() 分配。如果你用中间集合链接 .map().collect().iter().map().collect(),你在支付循环版本避免的分配。修复:通过直接链接适配器消除中间 collect,或者如果你需要中间集合用于其他原因使用循环。
8.9 味觉测试:转换目录
这里是常见“我写了 6 行但有一行“模式的参考表:
| 命令式模式 | 函数式等价物 | 何时偏好函数式 |
|---|---|---|
if let Some(x) = opt { f(x) } else { default } | opt.map_or(default, f) | 两边短表达式 |
if let Some(x) = opt { Some(g(x)) } else { None } | opt.map(g) | 总是 —— 这就是 map 的目的 |
if condition { Some(x) } else { None } | condition.then_some(x) | 总是 |
if condition { Some(compute()) } else { None } | condition.then(compute) | 总是 |
match opt { Some(x) if pred(x) => Some(x), _ => None } | opt.filter(pred) | 总是 |
for x in iter { if pred(x) { result.push(f(x)); } } | iter.filter(pred).map(f).collect() | 当管道在一屏内可读 |
if a.is_some() && b.is_some() { Some((a?, b?)) } | a.zip(b) | 总是 —— .zip() 正是这个 |
match (a, b) { (Some(x), Some(y)) => x + y, _ => 0 } | a.zip(b).map(|(x,y)| x + y).unwrap_or(0) | 判断调用 —— 取决于复杂度 |
iter.map(f).collect::<Vec<_>>()[0] | iter.map(f).next().unwrap() | 不要为一个元素分配 Vec |
let mut v = vec; v.sort(); v | { let mut v = vec; v.sort(); v } | Rust 在 std 中没有 .sorted()(使用 itertools) |
8.10 反模式
过度函数式化:没人能读的 5 层深链
#![allow(unused)]
fn main() {
// 这不优雅。这是谜题。
let result = data.iter()
.filter_map(|x| x.metadata.as_ref())
.flat_map(|m| m.tags.iter())
.filter(|t| t.starts_with("env:"))
.map(|t| t.strip_prefix("env:").unwrap())
.filter(|env| allowed_envs.contains(env))
.map(|env| env.to_uppercase())
.collect::<HashSet<_>>()
.into_iter()
.sorted()
.collect::<Vec<_>>();
}
当链超过 ~4 个适配器,用命名的中间变量分解或提取 helper:
#![allow(unused)]
fn main() {
let env_tags = data.iter()
.filter_map(|x| x.metadata.as_ref())
.flat_map(|m| m.tags.iter());
let allowed: Vec<_> = env_tags
.filter_map(|t| t.strip_prefix("env:"))
.filter(|env| allowed_envs.contains(env))
.map(|env| env.to_uppercase())
.sorted()
.collect();
}
函数式不足:Rust 有词汇的 C 风格循环
#![allow(unused)]
fn main() {
// 这只是 .any()
let mut found = false;
for item in &list {
if item.is_expired() {
found = true;
break;
}
}
// 写这个代替
let found = list.iter().any(|item| item.is_expired());
}
#![allow(unused)]
fn main() {
// 这只是 .find()
let mut target = None;
for server in &fleet {
if server.id == target_id {
target = Some(server);
break;
}
}
// 写这个代替
let target = fleet.iter().find(|s| s.id == target_id);
}
#![allow(unused)]
fn main() {
// 这只是 .all()
let mut all_healthy = true;
for server in &fleet {
if !server.is_healthy() {
all_healthy = false;
break;
}
}
// 写这个代替
let all_healthy = fleet.iter().all(|s| s.is_healthy());
}
标准库有这些是有原因的。学习词汇,模式变得明显。
关键要点
- Option 和 Result 是单元素集合。 它们的组合器(
.map()、.and_then()、.unwrap_or_else()、.filter()、.zip())替换大多数if let/match样板。- 使用
bool::then_some()—— 它在每种情况下替换if cond { Some(x) } else { None }。- 迭代器链赢在数据管道 —— filter/map/collect 带零可变状态。它们编译到与循环相同的机器码。
- 循环赢在多输出状态机 —— 当你构建多个集合、在分支中做 I/O、或管理状态转换。
?操作符是两全其美 —— 函数式错误传播带命令式可读性。- 在 ~4 个适配器处分解链 —— 用命名的中间物提高可读性。过度函数式化与函数式不足一样糟。
- 学习标准库词汇 ——
.any()、.all()、.find()、.position()、.sum()、.min_by_key()—— 每个都用单个揭示意图的调用替换多行循环。
另见: 第 7 章 了解闭包机制和
Fntrait 层次结构。第 10 章 了解错误组合器模式。第 15 章 了解流畅 API 设计。
练习:重构命令式到函数式 ★★(约 30 分钟)
将以下函数从命令式重构为函数式风格。然后识别函数式版本更糟的一个地方并解释为什么。
#![allow(unused)]
fn main() {
fn summarize_fleet(fleet: &[Server]) -> FleetSummary {
let mut healthy = Vec::new();
let mut degraded = Vec::new();
let mut failed = Vec::new();
let mut total_power = 0.0;
let mut max_temp = f64::NEG_INFINITY;
for server in fleet {
match server.health_status() {
Health::Healthy => healthy.push(server.id.clone()),
Health::Degraded(reason) => degraded.push((server.id.clone(), reason)),
Health::Failed(err) => failed.push((server.id.clone(), err)),
}
total_power += server.power_draw();
if server.max_temperature() > max_temp {
max_temp = server.max_temperature();
}
}
FleetSummary {
healthy,
degraded,
failed,
avg_power: total_power / fleet.len() as f64,
max_temp,
}
}
}
🔑 解答
total_power 和 max_temp 是干净的函数式重写:
#![allow(unused)]
fn main() {
fn summarize_fleet(fleet: &[Server]) -> FleetSummary {
let avg_power: f64 = fleet.iter().map(|s| s.power_draw()).sum::<f64>()
/ fleet.len() as f64;
let max_temp = fleet.iter()
.map(|s| s.max_temperature())
.fold(f64::NEG_INFINITY, f64::max);
// 但三路分区 BETTER 作为循环。
// 函数式版本将需要三个独立的 pass
// 或带三个可变累加器的 awkward fold。
let mut healthy = Vec::new();
let mut degraded = Vec::new();
let mut failed = Vec::new();
for server in fleet {
match server.health_status() {
Health::Healthy => healthy.push(server.id.clone()),
Health::Degraded(reason) => degraded.push((server.id.clone(), reason)),
Health::Failed(err) => failed.push((server.id.clone(), err)),
}
}
FleetSummary { healthy, degraded, failed, avg_power, max_temp }
}
}
为什么循环对三路分区更好: 函数式版本将需要三个 .filter().collect() pass(3x 迭代),或带 tuple 内三个 mut Vec 累加器的 .fold() —— 这只是用更糟语法重写的循环。命令式单 pass 循环更清晰、更高效、更易扩展。
9. Smart Pointers and Interior Mutability 🟡
你将学到:
- Box、Rc、Arc 用于堆分配和共享所有权
- Weak 引用用于打破 Rc/Arc 引用循环
- Cell、RefCell 和 Cow 用于内部可变性模式
- Pin 用于自引用类型和 ManuallyDrop 用于生命周期控制
Box、Rc、Arc — 堆分配和共享
#![allow(unused)]
fn main() {
// --- Box<T>: 单一所有者,堆分配 ---
// 使用场景:递归类型、大值、trait objects
let boxed: Box<i32> = Box::new(42);
println!("{}", *boxed); // Deref 到 i32
// 递归类型需要 Box(否则无限大小):
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
// Trait object(动态 dispatch):
let writer: Box<dyn std::io::Write> = Box::new(std::io::stdout());
// --- Rc<T>: 多所有者,单线程 ---
// 使用场景:一个线程内的共享所有权(无 Send/Sync)
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // 增加引用计数(NOT 深克隆)
let c = Rc::clone(&a);
println!("Ref count: {}", Rc::strong_count(&a)); // 3
// 所有三个指向同一个 Vec。当最后一个 Rc drop 时,
// Vec 被释放。
// --- Arc<T>: 多所有者,线程安全 ---
// 使用场景:跨线程共享所有权
use std::sync::Arc;
let shared = Arc::new(String::from("shared data"));
let handles: Vec<_> = (0..5).map(|_| {
let shared = Arc::clone(&shared);
std::thread::spawn(move || println!("{shared}"))
}).collect();
for h in handles { h.join().unwrap(); }
}
Weak 引用 —— 打破引用循环
Rc 和 Arc 使用引用计数,无法释放循环(A → B → A)。
Weak<T> 是不增加强引用的非拥有 handle:
#![allow(unused)]
fn main() {
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 不让父节点存活
children: RefCell<Vec<Rc<Node>>>,
}
let parent = Rc::new(Node {
value: 0, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
value: 1, parent: RefCell::new(Rc::downgrade(&parent)), children: RefCell::new(vec![]),
});
parent.children.borrow_mut().push(Rc::clone(&child));
// 从子节点访问父节点 —— 返回 Option<Rc<Node>>:
if let Some(p) = child.parent.borrow().upgrade() {
println!("Child's parent value: {}", p.value); // 0
}
// 当 `parent` drop 时,strong_count → 0,内存释放。
// `child.parent.upgrade()` 将返回 `None`。
}
经验法则:对所有权边使用 Rc/Arc,对后向引用和缓存使用 Weak。对于线程安全代码,使用 Arc<T> 带 sync::Weak<T>。
Cell 和 RefCell —— 内部可变性
有时你需要在共享(&)引用后可变数据。Rust 提供内部可变性带运行时借用检查:
#![allow(unused)]
fn main() {
use std::cell::{Cell, RefCell};
// --- Cell<T>: 基于 Copy 的内部可变性 ---
// 仅用于 Copy 类型(或你 swap in/out 的类型)
struct Counter {
count: Cell<u32>,
}
impl Counter {
fn new() -> Self { Counter { count: Cell::new(0) } }
fn increment(&self) { // &self,不是 &mut self!
self.count.set(self.count.get() + 1);
}
fn value(&self) -> u32 { self.count.get() }
}
// --- RefCell<T>: 运行时借用检查 ---
// 如果你在运行时违反借用规则则 panic
struct Cache {
data: RefCell<Vec<String>>,
}
impl Cache {
fn new() -> Self { Cache { data: RefCell::new(Vec::new()) } }
fn add(&self, item: String) { // &self —— 外部看是不可变的
self.data.borrow_mut().push(item); // 运行时检查的 &mut
}
fn get_all(&self) -> Vec<String> {
self.data.borrow().clone() // 运行时检查的 &
}
fn bad_example(&self) {
let _guard1 = self.data.borrow();
// let _guard2 = self.data.borrow_mut();
// ❌ 运行时 PANICS —— 当 & 存在时不能有 &mut
}
}
}
Cell vs RefCell:
Cell从不会 panic(它复制/交换值)但只 适用于Copy类型或通过swap()/replace()。RefCell适用于任何 类型但在双重可变借用时 panic。两者都不是Sync—— 对于多线程 使用,参见Mutex/RwLock。
Cow —— 写时克隆
Cow(Clone on Write)持有借用或拥有的值。它仅在需要可变时克隆:
use std::borrow::Cow;
// 避免在不需要修改时分配:
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains('\t') {
// 仅当 tab 需要替换时分配
Cow::Owned(input.replace('\t', " "))
} else {
// 无分配 —— 只返回引用
Cow::Borrowed(input)
}
}
fn main() {
let clean = "no tabs here";
let dirty = "tabs\there";
let r1 = normalize(clean); // Cow::Borrowed —— 零分配
let r2 = normalize(dirty); // Cow::Owned —— 分配新 String
println!("{r1}");
println!("{r2}");
}
// 也对可能不需要所有权的函数参数有用:
fn process(data: Cow<'_, [u8]>) {
// 可以读取数据无需复制
println!("Length: {}", data.len());
// 如果我们需要可变,Cow 自动克隆:
let mut owned = data.into_owned(); // 仅当 Borrowed 时克隆
owned.push(0xFF);
}
Cow<'_, [u8]> 用于二进制数据
Cow 对字节导向的 API 特别有用,其中数据可能需要或不需要转换(校验和插入、填充、转义)。这避免在常见快速路径上分配 Vec<u8>:
#![allow(unused)]
fn main() {
use std::borrow::Cow;
/// 将帧填充到最小长度,当无需填充时借用。
fn pad_frame(frame: &[u8], min_len: usize) -> Cow<'_, [u8]> {
if frame.len() >= min_len {
Cow::Borrowed(frame) // 已经足够长 —— 零分配
} else {
let mut padded = frame.to_vec();
padded.resize(min_len, 0x00);
Cow::Owned(padded) // 仅当需要填充时分配
}
}
let short = pad_frame(&[0xDE, 0xAD], 8); // Owned —— 填充到 8 字节
let long = pad_frame(&[0; 64], 8); // Borrowed —— 已经 >= 8
}
提示:当你需要可能转换的缓冲区的引用计数共享时,组合
Cow<[u8]>与bytes::Bytes(第 10 章)。
何时使用哪个指针
| 指针 | 所有者数量 | 线程安全 | 可变性 | 何时使用 |
|---|---|---|---|---|
Box<T> | 1 | ✅ (如果 T: Send) | 通过 &mut | 堆分配、trait objects、递归类型 |
Rc<T> | N | ❌ | 无(包装在 Cell/RefCell) | 共享所有权、单线程、图/树 |
Arc<T> | N | ✅ | 无(包装在 Mutex/RwLock) | 跨线程共享所有权 |
Cell<T> | — | ❌ | .get() / .set() | Copy 类型的内部可变性 |
RefCell<T> | — | ❌ | .borrow() / .borrow_mut() | 任何类型的内部可变性,单线程 |
Cow<'_, T> | 0 或 1 | ✅ (如果 T: Send) | 写时克隆 | 当数据经常不变时避免分配 |
Pin 和自引用类型
Pin<P> 防止值在内存中移动。这对于自引用类型 —— 包含指向自己数据的指针的结构体 —— 和 Futures 是必需的,它们可能在 .await 点后持有引用。
use std::pin::Pin;
use std::marker::PhantomPinned;
// 自引用结构体(简化):
struct SelfRef {
data: String,
ptr: *const String, // 指向 `data` 上面
_pin: PhantomPinned, // 选择退出 Unpin —— 不能移动
}
impl SelfRef {
fn new(s: &str) -> Pin<Box<Self>> {
let val = SelfRef {
data: s.to_string(),
ptr: std::ptr::null(),
_pin: PhantomPinned,
};
let mut boxed = Box::pin(val);
// 安全性:设置指针后我们不移动数据
let self_ptr: *const String = &boxed.data;
unsafe {
let mut_ref = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).ptr = self_ptr;
}
boxed
}
fn data(&self) -> &str {
&self.data
}
fn ptr_data(&self) -> &str {
// 安全性:ptr 设置为在 pinned 时指向 self.data
unsafe { &*self.ptr }
}
}
fn main() {
let pinned = SelfRef::new("hello");
assert_eq!(pinned.data(), pinned.ptr_data()); // 都是 "hello"
// std::mem::swap 将无效 ptr —— 但 Pin 防止它
}
关键概念:
| 概念 | 含义 |
|---|---|
Unpin (auto-trait) | “移动这个类型是安全的。” 大多数类型默认是 Unpin。 |
!Unpin / PhantomPinned | “我有内部指针 —— 不要移动我。” |
Pin<&mut T> | 可变引用保证 T 不移动 |
Pin<Box<T>> | 拥有的、堆 pinned 的值 |
为什么这对 async 重要:每个 async fn 脱糖到 Future,可能在 .await 点后持有引用 —— 使其自引用。async 运行时使用 Pin<&mut Future> 保证 future 在 poll 后不移动。
#![allow(unused)]
fn main() {
// 当你写:
async fn fetch(url: &str) -> String {
let response = http_get(url).await; // 跨越 await 持有引用
response.text().await
}
// 编译器生成状态机结构体是 !Unpin,
// 运行时在调用 Future::poll() 之前 pinned 它。
}
何时关心 Pin:(1) 手动实现
Future,(2) 编写 async 运行时或组合器,(3) 任何带自引用指针的结构体。对于普通应用代码,async/await透明地处理 pinned。参见配套Async Rust 培训了解更多深入内容。Crate 替代:对于无手动
Pin的自引用结构体,考虑ouroboros或self_cell—— 它们生成带正确 pinned 和 drop 语义的安全包装器。
Pin 投影 —— 结构 Pinned
当你有 Pin<&mut MyStruct>,你经常需要访问单独字段。Pin 投影是从 Pin<&mut Struct> 安全到 Pin<&mut Field>(对于 pinned 字段)或 &mut Field(对于 unpinned 字段)的模式。
问题:Pinned 类型上的字段访问
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::marker::PhantomPinned;
struct MyFuture {
data: String, // 普通字段 —— 移动安全
state: InternalState, // 自引用 —— 必须保持 pinned
_pin: PhantomPinned,
}
enum InternalState {
Waiting { ptr: *const String }, // 指向 `data` —— 自引用
Done,
}
// 给定 `Pin<&mut MyFuture>`,你如何访问 `data` 和 `state`?
// 你不能直接做 `pinned.data` —— 编译器不会让你
// 在 unsafe 情况下获取 pinned 值的 &mut 字段。
}
手动 Pin 投影(unsafe)
#![allow(unused)]
fn main() {
impl MyFuture {
// 投影到 `data` —— 这个字段结构上 unpinned(移动安全)
fn data(self: Pin<&mut Self>) -> &mut String {
// 安全性:`data` 不是结构 pinned。单独移动 `data`
// 不移动整个结构体,所以 Pin 的保证保留。
unsafe { &mut self.get_unchecked_mut().data }
}
// 投影到 `state` —— 这个字段是结构 pinned
fn state(self: Pin<&mut Self>) -> Pin<&mut InternalState> {
// 安全性:`state` 是结构 pinned —— 我们通过返回 Pin<&mut InternalState> 维持 pinned 不变量。
unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().state) }
}
}
}
结构 pinned 规则 —— 字段是“结构 pinned“如果:
- 移动/交换那个字段单独可能无效自引用
- 结构体的
Dropimpl 不能移动字段 - 结构体必须是
!Unpin(由PhantomPinned或!Unpin字段强制执行)
pin-project —— 安全 Pin 投影(零 Unsafe)
pin-project crate 在编译时生成可证明正确的投影,消除手动 unsafe 的需要:
#![allow(unused)]
fn main() {
use pin_project::pin_project;
use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};
#[pin_project] // <-- 生成投影方法
struct TimedFuture<F: Future> {
#[pin] // <-- 结构 pinned(它是 Future)
inner: F,
started_at: std::time::Instant, // NOT pinned —— 普通数据
}
impl<F: Future> Future for TimedFuture<F> {
type Output = (F::Output, std::time::Duration);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); // 安全!由 pin_project 生成
// this.inner : Pin<&mut F> —— pinned 字段
// this.started_at : &mut std::time::Instant —— unpinned 字段
match this.inner.poll(cx) {
Poll::Ready(output) => {
let elapsed = this.started_at.elapsed();
Poll::Ready((output, elapsed))
}
Poll::Pending => Poll::Pending,
}
}
}
}
pin-project vs 手动投影
| 方面 | 手动(unsafe) | pin-project |
|---|---|---|
| 安全性 | 你证明不变量 | 编译器验证 |
| 样板 | 低(但易错) | 零 —— derive 宏 |
Drop 交互 | 不能移动 pinned 字段 | 强制执行:#[pinned_drop] |
| 编译时成本 | 无 | 过程宏展开 |
| 使用场景 | 原语、no_std | 应用/库代码 |
#[pinned_drop] —— Pinned 类型的 Drop
当类型有 #[pin] 字段,pin-project 需要 #[pinned_drop] 而不是常规 Drop impl 以防止意外移动 pinned 字段:
#![allow(unused)]
fn main() {
use pin_project::{pin_project, pinned_drop};
use std::pin::Pin;
#[pin_project(PinnedDrop)]
struct Connection<F> {
#[pin]
future: F,
buffer: Vec<u8>, // 非 pinned —— 可以在 drop 中移动
}
#[pinned_drop]
impl<F> PinnedDrop for Connection<F> {
fn drop(self: Pin<&mut Self>) {
let this = self.project();
// `this.future` 是 Pin<&mut F> —— 不能移动,只能原地 drop
// `this.buffer` 是 &mut Vec<u8> —— 可以清空、清除等
this.buffer.clear();
println!("Connection dropped, buffer cleared");
}
}
}
Pin 投影在实践中何时重要
graph TD
A["你手动实现 Future 吗?"] -->|是 | B["future 在 .await 点后<br/>持有引用?"]
A -->|否 | C["async/await 为你处理 Pin<br/>✅ 无需投影"]
B -->|是 | D["在你的 future<br/>结构体上使用 #[pin_project]"]
B -->|否 | E["你的 future 是 Unpin<br/>✅ 无需投影"]
D --> F["标记 futures/streams 为 #[pin]<br/>保留数据字段 unpinned"]
style C fill:#91e5a3,color:#000
style E fill:#91e5a3,color:#000
style D fill:#ffa07a,color:#000
style F fill:#ffa07a,color:#000
经验法则:如果你包装另一个
Future或Stream,使用pin-project。如果你用async/await编写应用代码,你永远不需要直接 pin 投影。参见配套Async Rust 培训了解使用 pin 投影的 async 组合器模式。
Drop 顺序和 ManuallyDrop
Rust 的 drop 顺序是确定性的但有值得知道的规则:
Drop 顺序规则
struct Label(&'static str);
impl Drop for Label {
fn drop(&mut self) { println!("Dropping {}", self.0); }
}
fn main() {
let a = Label("first"); // 第一个声明
let b = Label("second"); // 第二个声明
let c = Label("third"); // 第三个声明
}
// 输出:
// Dropping third ← 局部变量以 REVERSE 声明顺序 drop
// Dropping second
// Dropping first
三个规则:
| 什么 | Drop 顺序 | 理由 |
|---|---|---|
| 局部变量 | 反向声明顺序 | 后面的变量可能引用前面的 |
| 结构体字段 | 声明顺序(从上到下) | 匹配构造顺序(自 Rust 1.0 稳定,由 RFC 1857 保证) |
| Tuple 元素 | 声明顺序(从左到右) | (a, b, c) → drop a,然后 b,然后 c |
#![allow(unused)]
fn main() {
struct Server {
listener: Label, // 第 1 个 drop
handler: Label, // 第 2 个 drop
logger: Label, // 第 3 个 drop
}
// 字段从上到下 drop(声明顺序)。
// 当字段互相引用或持有资源时这很重要。
}
实际影响:如果你的结构体有
JoinHandle和Sender, 字段顺序决定哪个先 drop。如果线程从 channel 读取,先 dropSender(关闭 channel)以便线程退出,然后 join handle。在结构体中将Sender放在JoinHandle上面。
ManuallyDrop<T> —— 抑制自动 Drop
ManuallyDrop<T> 包装值并防止其析构函数自动运行。你负责 drop 它(或故意泄漏它):
#![allow(unused)]
fn main() {
use std::mem::ManuallyDrop;
// 使用场景 1:在 unsafe 代码中防止双重释放
struct TwoPhaseBuffer {
// 我们需要自己 drop Vec 来控制时机
data: ManuallyDrop<Vec<u8>>,
committed: bool,
}
impl TwoPhaseBuffer {
fn new(capacity: usize) -> Self {
TwoPhaseBuffer {
data: ManuallyDrop::new(Vec::with_capacity(capacity)),
committed: false,
}
}
fn write(&mut self, bytes: &[u8]) {
self.data.extend_from_slice(bytes);
}
fn commit(&mut self) {
self.committed = true;
println!("Committed {} bytes", self.data.len());
}
}
impl Drop for TwoPhaseBuffer {
fn drop(&mut self) {
if !self.committed {
println!("Rolling back — dropping uncommitted data");
}
// 安全性:data 在这里总是有效的;我们只 drop 一次。
unsafe { ManuallyDrop::drop(&mut self.data); }
}
}
}
#![allow(unused)]
fn main() {
// 使用场景 2:故意泄漏(例如,全局单例)
fn leaked_string() -> &'static str {
// Box::leak() 是创建 &'static 引用的习惯方式:
let s = String::from("lives forever");
Box::leak(s.into_boxed_str())
// ⚠️ 这是受控内存泄漏。String 的堆分配
// 永不被释放。仅用于长生命周期单例。
}
// ManuallyDrop 替代(需要 unsafe):
// ⚠️ 优先使用上面的 Box::leak() —— 这里仅展示
// ManuallyDrop 语义(抑制 Drop 同时堆数据存活)。
fn leaked_string_manual() -> &'static str {
use std::mem::ManuallyDrop;
let md = ManuallyDrop::new(String::from("lives forever"));
// 安全性:ManuallyDrop 防止释放;堆数据存活
// 永远,所以 'static 引用有效。
unsafe { &*(md.as_str() as *const str) }
}
}
#![allow(unused)]
fn main() {
// 使用场景 3:Union 字段(一次只有一个变体有效)
use std::mem::ManuallyDrop;
union IntOrString {
i: u64,
s: ManuallyDrop<String>,
// String 有 Drop impl,所以它 MUST 包装在 ManuallyDrop
// 在 union 内 —— 编译器不知道哪个字段活跃。
}
// 无自动 Drop —— 构造 IntOrString 的代码也必须
// 处理清理。如果 String 变体活跃,调用:
// unsafe { ManuallyDrop::drop(&mut value.s); }
// 无 Drop impl,union 只是泄漏(无 UB,只是泄漏)。
}
ManuallyDrop vs mem::forget:
ManuallyDrop<T> | mem::forget(value) | |
|---|---|---|
| 何时 | 在构造时包装 | 稍后消费 |
| 访问内部 | &*md / &mut *md | 值没了 |
| 稍后 Drop | ManuallyDrop::drop(&mut md) | 不可能 |
| 使用场景 | 细粒度生命周期控制 | 用过即弃泄漏 |
规则:在 unsafe 抽象中使用
ManuallyDrop,当你需要控制析构函数确切何时运行。在安全应用代码中,你几乎从不需要它 —— Rust 的自动 drop 顺序正确处理事情。
关键要点 —— Smart Pointers
Box用于堆上的单一所有权;Rc/Arc用于共享所有权(单/多线程)Cell/RefCell提供内部可变性;RefCell在运行时违反时 panicCow在常见路径上避免分配;Pin防止自引用类型移动- Drop 顺序:字段按声明顺序 drop(RFC 1857);局部变量按反向声明顺序 drop
另见: 第 6 章 — 并发 了解 Arc + Mutex 模式。第 4 章 — PhantomData 了解 PhantomData 与智能指针一起使用。
graph TD
Box["Box<T><br>单一所有者,堆"] --> Heap["堆分配"]
Rc["Rc<T><br>共享,单线程"] --> Heap
Arc["Arc<T><br>共享,多线程"] --> Heap
Rc --> Weak1["Weak<T><br>非拥有"]
Arc --> Weak2["Weak<T><br>非拥有"]
Cell["Cell<T><br>Copy 内部可变"] --> Stack["栈 / 内部"]
RefCell["RefCell<T><br>运行时借用检查"] --> Stack
Cow["Cow<T><br>写时克隆"] --> Stack
style Box fill:#d4efdf,stroke:#27ae60,color:#000
style Rc fill:#e8f4f8,stroke:#2980b9,color:#000
style Arc fill:#e8f4f8,stroke:#2980b9,color:#000
style Weak1 fill:#fef9e7,stroke:#f1c40f,color:#000
style Weak2 fill:#fef9e7,stroke:#f1c40f,color:#000
style Cell fill:#fdebd0,stroke:#e67e22,color:#000
style RefCell fill:#fdebd0,stroke:#e67e22,color:#000
style Cow fill:#fdebd0,stroke:#e67e22,color:#000
style Heap fill:#f5f5f5,stroke:#999,color:#000
style Stack fill:#f5f5f5,stroke:#999,color:#000
练习:引用计数图 ★★(约 30 分钟)
使用 Rc<RefCell<Node>> 构建有向图,其中每个节点有名称和子节点列表。使用 Weak 打破后向边创建循环(A → B → C → A)。用 Rc::strong_count 验证无内存泄漏。
🔑 解答
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
name: String,
children: Vec<Rc<RefCell<Node>>>,
back_ref: Option<Weak<RefCell<Node>>>,
}
impl Node {
fn new(name: &str) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node {
name: name.to_string(),
children: Vec::new(),
back_ref: None,
}))
}
}
impl Drop for Node {
fn drop(&mut self) {
println!("Dropping {}", self.name);
}
}
fn main() {
let a = Node::new("A");
let b = Node::new("B");
let c = Node::new("C");
// A → B → C,C 通过 Weak 后向引用 A
a.borrow_mut().children.push(Rc::clone(&b));
b.borrow_mut().children.push(Rc::clone(&c));
c.borrow_mut().back_ref = Some(Rc::downgrade(&a)); // Weak 引用!
println!("A strong count: {}", Rc::strong_count(&a)); // 1(只有 `a` 绑定)
println!("B strong count: {}", Rc::strong_count(&b)); // 2(b + A 的子节点)
println!("C strong count: {}", Rc::strong_count(&c)); // 2(c + B 的子节点)
// 升级 weak 引用证明它有效:
let c_ref = c.borrow();
if let Some(back) = &c_ref.back_ref {
if let Some(a_ref) = back.upgrade() {
println!("C points back to: {}", a_ref.borrow().name);
}
}
// 当 a, b, c 离开作用域,所有 Nodes drop(无循环泄漏!)
}
10. Error Handling Patterns 🟢
你将学到:
- 何时使用
thiserror(库)vsanyhow(应用程序)- 使用
#[from]和.context()包装器的错误转换链?运算符如何展开以及在main()中的工作原理- 何时 panic vs 返回错误,以及用于 FFI 边界的
catch_unwind
thiserror vs anyhow —— 库 vs 应用程序
Rust 错误处理围绕 Result<T, E> 类型展开。有两个 crate 占据主导地位:
// --- thiserror:用于 LIBRARIES ---
// 通过 derive 宏生成 Display、Error 和 From 实现
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("connection failed: {0}")]
ConnectionFailed(String),
#[error("query error: {source}")]
QueryError {
#[source]
source: sqlx::Error,
},
#[error("record not found: table={table} id={id}")]
NotFound { table: String, id: u64 },
#[error(transparent)] // 将 Display 委托给内部错误
Io(#[from] std::io::Error), // 自动生成 From<io::Error>
}
// --- anyhow:用于 APPLICATIONS ---
// 动态错误类型 —— 非常适合顶层代码,只需传播错误
use anyhow::{Context, Result, bail, ensure};
fn read_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
let config: Config = serde_json::from_str(&content)
.context("failed to parse config JSON")?;
ensure!(config.port > 0, "port must be positive, got {}", config.port);
Ok(config)
}
fn main() -> Result<()> {
let config = read_config("server.toml")?;
if config.name.is_empty() {
bail!("server name cannot be empty"); // 立即返回 Err
}
Ok(())
}
何时使用哪一个:
thiserror | anyhow | |
|---|---|---|
| 使用场景 | 库、共享 crate | 应用程序、二进制文件 |
| 错误类型 | 具体枚举 —— 调用者可以匹配 | anyhow::Error —— 不透明 |
| 工作量 | 定义你的错误枚举 | 只需使用 Result<T> |
| 向下转型 | 不需要 —— 模式匹配 | error.downcast_ref::<MyError>() |
错误转换链 (#[from])
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
}
// 现在 ? 自动转换:
fn fetch_and_parse(url: &str) -> Result<Config, AppError> {
let body = reqwest::blocking::get(url)?.text()?; // reqwest::Error → AppError::Http
let config: Config = serde_json::from_str(&body)?; // serde_json::Error → AppError::Json
Ok(config)
}
Context 和错误包装
为错误添加人类可读的上下文,同时不丢失原始错误:
use anyhow::{Context, Result};
fn process_file(path: &str) -> Result<Data> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {path}"))?;
let data = parse_content(&content)
.with_context(|| format!("failed to parse {path}"))?;
validate(&data)
.context("validation failed")?;
Ok(data)
}
// 错误输出:
// Error: validation failed
//
// Caused by:
// 0: failed to parse config.json
// 1: expected ',' at line 5 column 12
深入理解 ? 运算符
? 是 match + From 转换 + 提前返回的语法糖:
#![allow(unused)]
fn main() {
// 这样写:
let value = operation()?;
// 展开为:
let value = match operation() {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
// ^^^^^^^^^^^^^^
// 通过 From trait 自动转换
};
}
? 也适用于 Option(在返回 Option 的函数中):
#![allow(unused)]
fn main() {
fn find_user_email(users: &[User], name: &str) -> Option<String> {
let user = users.iter().find(|u| u.name == name)?; // 如果未找到返回 None
let email = user.email.as_ref()?; // 如果 email 为 None 返回 None
Some(email.to_uppercase())
}
}
Panic、catch_unwind 以及何时终止
#![allow(unused)]
fn main() {
// Panics:用于 BUGS,而非预期错误
fn get_element(data: &[i32], index: usize) -> &i32 {
// 如果这里 panic,说明是编程错误(bug)
// 不要"处理"它 —— 修复调用者
&data[index]
}
// catch_unwind:用于边界(FFI、线程池)
use std::panic;
let result = panic::catch_unwind(|| {
// 安全地运行可能 panic 的代码
risky_operation()
});
match result {
Ok(value) => println!("Success: {value:?}"),
Err(_) => eprintln!("Operation panicked — continuing safely"),
}
// 何时使用哪一个:
// - Result<T, E> → 预期失败(文件未找到、网络超时)
// - panic!() → 编程 bug(索引越界、违反不变量)
// - process::abort() → 不可恢复状态(安全违规、数据损坏)
}
C++ 对比:
Result<T, E>替代异常处理预期错误。panic!()像assert()或std::terminate()—— 用于 bug,而非 控制流。Rust 的?运算符使错误传播像异常一样顺手,同时 没有不可预测的控制流。
关键要点 —— 错误处理
- 库:
thiserror用于结构化错误枚举;应用程序:anyhow用于顺手的传播#[from]自动生成From实现;.context()添加人类可读的包装?展开为From::from()+ 提前返回;在返回Result的main()中可用
另见: 第 14 章 — API 设计 了解 “parse, don’t validate” 模式。第 11 章 — 序列化 了解 serde 错误处理。
flowchart LR
A["std::io::Error"] -->|"#[from]"| B["AppError::Io"]
C["serde_json::Error"] -->|"#[from]"| D["AppError::Json"]
E["Custom validation"] -->|"manual"| F["AppError::Validation"]
B --> G["? operator"]
D --> G
F --> G
G --> H["Result<T, AppError>"]
style A fill:#e8f4f8,stroke:#2980b9,color:#000
style C fill:#e8f4f8,stroke:#2980b9,color:#000
style E fill:#e8f4f8,stroke:#2980b9,color:#000
style B fill:#fdebd0,stroke:#e67e22,color:#000
style D fill:#fdebd0,stroke:#e67e22,color:#000
style F fill:#fdebd0,stroke:#e67e22,color:#000
style G fill:#fef9e7,stroke:#f1c40f,color:#000
style H fill:#d4efdf,stroke:#27ae60,color:#000
练习:使用 thiserror 的错误层次结构 ★★(约 30 分钟)
为一个文件处理应用程序设计错误类型层次结构,该应用程序可能在 I/O、解析(JSON 和 CSV)和验证期间失败。使用 thiserror 并演示 ? 传播。
🔑 解答
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("CSV error at line {line}: {message}")]
Csv { line: usize, message: String },
#[error("validation error: {field} — {reason}")]
Validation { field: String, reason: String },
}
fn read_file(path: &str) -> Result<String, AppError> {
Ok(std::fs::read_to_string(path)?) // io::Error → AppError::Io 通过 #[from]
}
fn parse_json(content: &str) -> Result<serde_json::Value, AppError> {
Ok(serde_json::from_str(content)?) // serde_json::Error → AppError::Json
}
fn validate_name(value: &serde_json::Value) -> Result<String, AppError> {
let name = value.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| AppError::Validation {
field: "name".into(),
reason: "must be a non-null string".into(),
})?;
if name.is_empty() {
return Err(AppError::Validation {
field: "name".into(),
reason: "must not be empty".into(),
});
}
Ok(name.to_string())
}
fn process_file(path: &str) -> Result<String, AppError> {
let content = read_file(path)?;
let json = parse_json(&content)?;
let name = validate_name(&json)?;
Ok(name)
}
fn main() {
match process_file("config.json") {
Ok(name) => println!("Name: {name}"),
Err(e) => eprintln!("Error: {e}"),
}
}
11. Serialization, Zero-Copy, and Binary Data 🟡
你将学到:
- serde 基础:derive 宏、属性和枚举表示
- 高性能读密集型工作负载的零拷贝反序列化
- serde 格式生态系统(JSON、TOML、bincode、MessagePack)
- 使用
repr(C)、zerocopy 和bytes::Bytes处理二进制数据
serde 基础
serde (SERialize/DEserialize) 是 Rust 的通用序列化框架。
它将数据模型(你的结构体)与格式(JSON、TOML、二进制)分离:
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct ServerConfig {
name: String,
port: u16,
#[serde(default)] // 如果缺失则使用 Default::default()
max_connections: usize,
#[serde(skip_serializing_if = "Option::is_none")]
tls_cert_path: Option<String>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 从 JSON 反序列化:
let json_input = r#"{
"name": "hw-diag",
"port": 8080
}"#;
let config: ServerConfig = serde_json::from_str(json_input)?;
println!("{config:?}");
// ServerConfig { name: "hw-diag", port: 8080, max_connections: 0, tls_cert_path: None }
// 序列化为 JSON:
let output = serde_json::to_string_pretty(&config)?;
println!("{output}");
// 相同的结构体,不同的格式 —— 无需代码改动:
let toml_input = r#"
name = "hw-diag"
port = 8080
"#;
let config: ServerConfig = toml::from_str(toml_input)?;
println!("{config:?}");
Ok(())
}
关键洞察:你的结构体只需 derive 一次
Serialize和Deserialize。 然后它可以与每个 serde 兼容的格式一起工作 —— JSON、TOML、YAML、 bincode、MessagePack、CBOR、postcard 等几十种格式。
常见 serde 属性
serde 通过字段和容器属性提供细粒度控制:
use serde::{Serialize, Deserialize};
// --- 容器属性(在结构体/枚举上)---
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // JSON 约定:field_name → fieldName
#[serde(deny_unknown_fields)] // 拒绝额外的键 —— 严格解析
struct DiagResult {
test_name: String, // 序列化为 "testName"
pass_count: u32, // 序列化为 "passCount"
fail_count: u32, // 序列化为 "failCount"
}
// --- 字段属性 ---
#[derive(Serialize, Deserialize)]
struct Sensor {
#[serde(rename = "sensor_id")] // 覆盖序列化时的字段名
id: u64,
#[serde(default)] // 如果输入中缺失则使用 Default
enabled: bool,
#[serde(default = "default_threshold")]
threshold: f64,
#[serde(skip)] // 从不序列化或反序列化
cached_value: Option<f64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
#[serde(flatten)] // 内联嵌套结构体字段
metadata: Metadata,
#[serde(with = "hex_bytes")] // 自定义序列化/反序列化模块
raw_data: Vec<u8>,
}
fn default_threshold() -> f64 { 1.0 }
#[derive(Serialize, Deserialize)]
struct Metadata {
vendor: String,
model: String,
}
// 使用 #[serde(flatten)],JSON 看起来像:
// { "sensor_id": 1, "vendor": "Intel", "model": "X200", ... }
// 而不是:{ "sensor_id": 1, "metadata": { "vendor": "Intel", ... } }
最常用属性速查表:
| 属性 | 级别 | 效果 |
|---|---|---|
rename_all = "camelCase" | 容器 | 将所有字段重命名为 camelCase/snake_case/SCREAMING_SNAKE_CASE |
deny_unknown_fields | 容器 | 遇到意外键时报错(严格模式) |
default | 字段 | 字段缺失时使用 Default::default() |
rename = "..." | 字段 | 自定义序列化名称 |
skip | 字段 | 完全排除在序列化/反序列化之外 |
skip_serializing_if = "fn" | 字段 | 条件排除(例如 Option::is_none) |
flatten | 字段 | 内联嵌套结构体的字段 |
with = "module" | 字段 | 使用自定义序列化/反序列化函数 |
alias = "..." | 字段 | 反序列化时接受替代名称 |
deserialize_with = "fn" | 字段 | 仅自定义反序列化函数 |
untagged | 枚举 | 按顺序尝试每个变体(输出中无判别式) |
枚举表示
serde 为 JSON 等格式中的枚举提供四种表示:
use serde::{Serialize, Deserialize};
// 1. 外部标记(默认):
#[derive(Serialize, Deserialize)]
enum Command {
Reboot,
RunDiag { test_name: String, timeout_secs: u64 },
SetFanSpeed(u8),
}
// "Reboot" → Command::Reboot
// {"RunDiag": {"test_name": "gpu", "timeout_secs": 60}} → Command::RunDiag { ... }
// 2. 内部标记 —— #[serde(tag = "type")]:
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
Start { timestamp: u64 },
Error { code: i32, message: String },
End { timestamp: u64, success: bool },
}
// {"type": "Start", "timestamp": 1706000000}
// {"type": "Error", "code": 42, "message": "timeout"}
// 3. 相邻标记 —— #[serde(tag = "t", content = "c")]:
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum Payload {
Text(String),
Binary(Vec<u8>),
}
// {"t": "Text", "c": "hello"}
// {"t": "Binary", "c": [0, 1, 2]}
// 4. 无标记 —— #[serde(untagged)]:
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum StringOrNumber {
Str(String),
Num(f64),
}
// "hello" → StringOrNumber::Str("hello")
// 42.0 → StringOrNumber::Num(42.0)
// ⚠️ 按顺序尝试 —— 第一个匹配的变体获胜
选择哪种表示:对于大多数 JSON API 使用内部标记(
tag = "type") —— 它最易读,符合 Go、Python 和 TypeScript 的约定。仅当 形状本身足以区分时才使用无标记。
零拷贝反序列化
serde 可以在反序列化时不分配新字符串 —— 直接从输入缓冲区借用。 这是高性能解析的关键:
use serde::Deserialize;
// --- _owned_(分配)---
// 每个 String 字段将字节从输入复制到新的堆分配中。
#[derive(Deserialize)]
struct OwnedRecord {
name: String, // 分配新的 String
value: String, // 分配另一个 String
}
// --- 零拷贝(借用)---
// &'de str 字段直接从输入借用 —— 零分配。
#[derive(Deserialize)]
struct BorrowedRecord<'a> {
name: &'a str, // 指向输入缓冲区
value: &'a str, // 指向输入缓冲区
}
fn main() {
let input = r#"{"name": "cpu_temp", "value": "72.5"}"#;
// Owned:分配两个 String 对象
let owned: OwnedRecord = serde_json::from_str(input).unwrap();
// 零拷贝:`name` 和 `value` 指向 `input` —— 无分配
let borrowed: BorrowedRecord = serde_json::from_str(input).unwrap();
// 输出有生命周期绑定:borrowed 不能超出 input 存活
println!("{}: {}", borrowed.name, borrowed.value);
}
理解生命周期:
// Deserialize<'de> —— 结构体可以从生命周期为 'de 的数据借用:
// struct BorrowedRecord<'a> where 'a == 'de
// 仅当输入缓冲区活得足够长时才有效
// DeserializeOwned —— 结构体拥有所有数据,不借用:
// trait DeserializeOwned: for<'de> Deserialize<'de> {}
// 适用于任何输入生命周期(结构体是独立的)
use serde::de::DeserializeOwned;
// 这个函数需要 owned 类型 —— 输入可以是临时的
fn parse_owned<T: DeserializeOwned>(input: &str) -> T {
serde_json::from_str(input).unwrap()
}
// 这个函数允许借用 —— 更高效但限制了生命周期
fn parse_borrowed<'a, T: Deserialize<'a>>(input: &'a str) -> T {
serde_json::from_str(input).unwrap()
}
何时使用零拷贝:
- 解析大文件且只需要少数几个字段时
- 高吞吐量管道(网络包、日志行)
- 当输入缓冲区已经活得足够长时(例如内存映射文件)
何时不使用零拷贝:
- 输入是短暂的(重用的网络读缓冲区)
- 需要在输入的生命周期之外存储结果
- 字段需要转换(转义、规范化)
实用技巧:
Cow<'a, str>给你两全其美 —— 可能时借用, 必要时分配(例如 JSON 转义序列需要解转义时)。serde 原生支持 Cow。
格式生态系统
| 格式 | Crate | 人类可读 | 大小 | 速度 | 使用场景 |
|---|---|---|---|---|---|
| JSON | serde_json | ✅ | 大 | 好 | 配置文件、REST API、日志 |
| TOML | toml | ✅ | 中 | 好 | 配置文件(Cargo.toml 风格) |
| YAML | serde_yaml | ✅ | 中 | 好 | 配置文件(复杂嵌套) |
| bincode | bincode | ❌ | 小 | 快 | IPC、缓存、Rust 到 Rust |
| postcard | postcard | ❌ | 极小 | 极快 | 嵌入式系统、no_std |
| MessagePack | rmp-serde | ❌ | 小 | 快 | 跨语言二进制协议 |
| CBOR | ciborium | ❌ | 小 | 快 | IoT、受限环境 |
#![allow(unused)]
fn main() {
// 相同的结构体,多种格式 —— serde 的力量:
#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct DiagConfig {
name: String,
tests: Vec<String>,
timeout_secs: u64,
}
let config = DiagConfig {
name: "accel_diag".into(),
tests: vec!["memory".into(), "compute".into()],
timeout_secs: 300,
};
// JSON: {"name":"accel_diag","tests":["memory","compute"],"timeout_secs":300}
let json = serde_json::to_string(&config).unwrap(); // 67 字节
// bincode:紧凑二进制 —— 约 40 字节,无字段名
let bin = bincode::serialize(&config).unwrap(); // 更小
// postcard:更小,varint 编码 —— 适合嵌入式
// let post = postcard::to_allocvec(&config).unwrap();
}
选择你的格式:
- 人类编辑的配置文件 → TOML 或 JSON
- Rust 到 Rust IPC/缓存 → bincode(快、紧凑、非跨语言)
- 跨语言二进制 → MessagePack 或 CBOR
- 嵌入式 /
no_std→ postcard
二进制数据和 repr(C)
对于硬件诊断,解析二进制协议数据很常见。Rust 提供 工具来安全、零拷贝地处理二进制数据:
#![allow(unused)]
fn main() {
// --- #[repr(C)]:可预测的内存布局 ---
// 确保字段按照 C 填充规则按声明顺序布局。
// 对于匹配硬件寄存器布局和协议头至关重要。
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct IpmiHeader {
rs_addr: u8,
net_fn_lun: u8,
checksum: u8,
rq_addr: u8,
rq_seq_lun: u8,
cmd: u8,
}
// --- 安全二进制解析与手动反序列化 ---
impl IpmiHeader {
fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < size_of::<Self>() {
return None;
}
Some(IpmiHeader {
rs_addr: data[0],
net_fn_lun: data[1],
checksum: data[2],
rq_addr: data[3],
rq_seq_lun: data[4],
cmd: data[5],
})
}
fn net_fn(&self) -> u8 { self.net_fn_lun >> 2 }
fn lun(&self) -> u8 { self.net_fn_lun & 0x03 }
}
// --- 感知字节序的解析 ---
fn read_u16_le(data: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([data[offset], data[offset + 1]])
}
fn read_u32_be(data: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
data[offset], data[offset + 1],
data[offset + 2], data[offset + 3],
])
}
// --- #[repr(C, packed)]:移除填充(对齐 = 1)---
#[repr(C, packed)]
#[derive(Debug, Clone, Copy)]
struct PcieCapabilityHeader {
cap_id: u8, // 能力 ID
next_cap: u8, // 指向下一个能力的指针
cap_reg: u16, // 特定于能力的寄存器
}
// ⚠️ 打包结构体:取 `&field` 会创建未对齐的引用 —— UB。
// 总是复制字段出来:let id = header.cap_id; // OK (Copy)
// 绝不要这样做:let r = &header.cap_reg; // 如果未对齐则 UB
}
zerocopy 和 bytemuck —— 安全转换
与其使用 unsafe transmute,不如使用在编译时验证布局安全性的 crate:
#![allow(unused)]
fn main() {
// --- zerocopy:编译时检查的零拷贝转换 ---
// Cargo.toml: zerocopy = { version = "0.8", features = ["derive"] }
use zerocopy::{FromBytes, IntoBytes, KnownLayout, Immutable};
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable, Debug)]
#[repr(C)]
struct SensorReading {
sensor_id: u16,
flags: u8,
_reserved: u8,
value: u32, // 定点数:actual = value / 1000.0
}
fn parse_sensor(raw: &[u8]) -> Option<&SensorReading> {
// 安全零拷贝:在编译时验证对齐和大小
SensorReading::ref_from_bytes(raw).ok()
// 返回指向 raw 内部的 &SensorReading —— 无拷贝、无分配
}
// --- bytemuck:简单、久经考验 ---
// Cargo.toml: bytemuck = { version = "1", features = ["derive"] }
use bytemuck::{Pod, Zeroable};
#[derive(Pod, Zeroable, Clone, Copy, Debug)]
#[repr(C)]
struct GpuRegister {
address: u32,
value: u32,
}
fn cast_registers(data: &[u8]) -> &[GpuRegister] {
// 安全转换:Pod 保证所有位模式都有效
bytemuck::cast_slice(data)
}
}
何时使用哪一个:
| 方法 | 安全性 | 开销 | 使用场景 |
|---|---|---|---|
| 手动逐字段解析 | ✅ 安全 | 复制字段 | 小结构体、复杂布局 |
zerocopy | ✅ 安全 | 零拷贝 | 大缓冲区、多次读取、编译时检查 |
bytemuck | ✅ 安全 | 零拷贝 | 简单 Pod 类型、转换切片 |
unsafe { transmute() } | ❌ 不安全 | 零拷贝 | 最后手段 —— 避免在应用程序代码中使用 |
bytes::Bytes —— 引用计数缓冲区
bytes crate(被 tokio、hyper、tonic 使用)提供零拷贝字节缓冲区
和引用计数 —— Bytes 对于 Vec<u8> 就像 Arc<[u8]> 对于 owned 切片:
use bytes::{Bytes, BytesMut, Buf, BufMut};
fn main() {
// --- BytesMut:用于构建数据的可变缓冲区 ---
let mut buf = BytesMut::with_capacity(1024);
buf.put_u8(0x01); // 写入一个字节
buf.put_u16(0x1234); // 写入 u16(大端)
buf.put_slice(b"hello"); // 写入原始字节
buf.put(&b"world"[..]); // 从切片写入
// 冻结为不可变 Bytes(零成本):
let data: Bytes = buf.freeze();
// --- Bytes:不可变、引用计数、可克隆 ---
let data2 = data.clone(); // 便宜:增加引用计数,非深拷贝
let slice = data.slice(3..8); // 零拷贝子切片(共享缓冲区)
// 使用 Buf trait 从 Bytes 读取:
let mut reader = &data[..];
let byte = reader.get_u8(); // 0x01
let short = reader.get_u16(); // 0x1234
// 不复制地分割:
let mut original = Bytes::from_static(b"HEADER\x00PAYLOAD");
let header = original.split_to(6); // header = "HEADER", original = "\x00PAYLOAD"
println!("header: {:?}", &header[..]);
println!("payload: {:?}", &original[1..]);
}
bytes vs Vec<u8>:
| 特性 | Vec<u8> | Bytes |
|---|---|---|
| Clone 成本 | O(n) 深拷贝 | O(1) 引用计数增加 |
| 子切片 | 带生命周期的借用 | 所有的、引用计数追踪 |
| 线程安全 | 非 Sync(需要 Arc) | 内置 Send + Sync |
| 可变性 | 直接 &mut | 先分割为 BytesMut |
| 生态系统 | 标准库 | tokio、hyper、tonic、axum |
何时使用 bytes:网络协议、包解析、任何你需要接收缓冲区 并将其分割为由不同组件或线程处理的部分的场景。 零拷贝分割是杀手级特性。
关键要点 —— 序列化和二进制数据
- serde 的 derive 宏处理 90% 的情况;对其余情况使用属性(
rename、skip、default)- 零拷贝反序列化(结构体中的
&'a str)避免读密集型工作负载的分配repr(C)+zerocopy/bytemuck用于硬件寄存器布局;bytes::Bytes用于引用计数缓冲区
另见: 第 9 章 — 错误处理 了解如何将 serde 错误与
thiserror组合。第 11 章 — Unsafe 了解repr(C)和 FFI 数据布局。
flowchart LR
subgraph Input
JSON["JSON"]
TOML["TOML"]
Bin["bincode"]
MsgP["MessagePack"]
end
subgraph serde["serde data model"]
Ser["Serialize"]
De["Deserialize"]
end
subgraph Output
Struct["Rust struct"]
Enum["Rust enum"]
end
JSON --> De
TOML --> De
Bin --> De
MsgP --> De
De --> Struct
De --> Enum
Struct --> Ser
Enum --> Ser
Ser --> JSON
Ser --> Bin
style JSON fill:#e8f4f8,stroke:#2980b9,color:#000
style TOML fill:#e8f4f8,stroke:#2980b9,color:#000
style Bin fill:#e8f4f8,stroke:#2980b9,color:#000
style MsgP fill:#e8f4f8,stroke:#2980b9,color:#000
style Ser fill:#fef9e7,stroke:#f1c40f,color:#000
style De fill:#fef9e7,stroke:#f1c40f,color:#000
style Struct fill:#d4efdf,stroke:#27ae60,color:#000
style Enum fill:#d4efdf,stroke:#27ae60,color:#000
练习:自定义 serde 反序列化 ★★★(约 45 分钟)
设计一个 HumanDuration 包装器,使用自定义 serde 反序列化器从人类可读的字符串如 "30s"、"5m"、"2h" 反序列化。它也应该序列化为相同的格式。
🔑 解答
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
struct HumanDuration(std::time::Duration);
impl HumanDuration {
fn from_str(s: &str) -> Result<Self, String> {
let s = s.trim();
if s.is_empty() { return Err("empty duration string".into()); }
let (num_str, suffix) = s.split_at(
s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len())
);
let value: u64 = num_str.parse()
.map_err(|_| format!("invalid number: {num_str}"))?;
let duration = match suffix {
"s" | "sec" => std::time::Duration::from_secs(value),
"m" | "min" => std::time::Duration::from_secs(value * 60),
"h" | "hr" => std::time::Duration::from_secs(value * 3600),
"ms" => std::time::Duration::from_millis(value),
other => return Err(format!("unknown suffix: {other}")),
};
Ok(HumanDuration(duration))
}
}
impl fmt::Display for HumanDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let secs = self.0.as_secs();
if secs == 0 {
write!(f, "{}ms", self.0.as_millis())
} else if secs % 3600 == 0 {
write!(f, "{}h", secs / 3600)
} else if secs % 60 == 0 {
write!(f, "{}m", secs / 60)
} else {
write!(f, "{}s", secs)
}
}
}
impl Serialize for HumanDuration {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for HumanDuration {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
HumanDuration::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Deserialize, Serialize)]
struct Config {
timeout: HumanDuration,
retry_interval: HumanDuration,
}
fn main() {
let json = r#"{ "timeout": "30s", "retry_interval": "5m" }"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.timeout.0, std::time::Duration::from_secs(30));
assert_eq!(config.retry_interval.0, std::time::Duration::from_secs(300));
let serialized = serde_json::to_string(&config).unwrap();
assert!(serialized.contains("30s"));
println!("Config: {serialized}");
}
12. Unsafe Rust — Controlled Danger 🔴
你将学到:
- 五种 unsafe 超能力以及何时需要每一种
- 编写可靠的抽象:安全的 API、不安全的内部实现
- 从 Rust 调用 C(以及反向调用)的 FFI 模式
- 常见的 UB 陷阱和 arena/slab 分配器模式
五种 Unsafe 超能力
unsafe 解锁了编译器无法验证的五种操作:
#![allow(unused)]
fn main() {
// SAFETY: 每个操作都在内联解释 below.
unsafe {
// 1. 解引用原始指针
let ptr: *const i32 = &42;
let value = *ptr; // 可能是悬空/null 指针
// 2. 调用 unsafe 函数
let layout = std::alloc::Layout::new::<u64>();
let mem = std::alloc::alloc(layout);
// 3. 访问可变静态变量
static mut COUNTER: u32 = 0;
COUNTER += 1; // 如果多个线程访问则存在数据竞争
// 4. 实现 unsafe trait
// unsafe impl Send for MyType {}
// 5. 访问 union 的字段
// union IntOrFloat { i: i32, f: f32 }
// let u = IntOrFloat { i: 42 };
// let f = u.f; // 重新解释位 —— 可能是垃圾
}
}
关键原则:
unsafe不会关闭借用检查器或类型系统。 它只解锁这五种特定能力。所有其他 Rust 规则仍然适用。
编写可靠的抽象
unsafe 的目的是为不安全操作构建安全的抽象:
#![allow(unused)]
fn main() {
/// 固定容量的栈分配缓冲区。
/// 所有公共方法都是安全的 —— unsafe 被封装。
pub struct StackBuf<T, const N: usize> {
data: [std::mem::MaybeUninit<T>; N],
len: usize,
}
impl<T, const N: usize> StackBuf<T, N> {
pub fn new() -> Self {
StackBuf {
// 每个元素都是独立的 MaybeUninit —— 不需要 unsafe。
// `const { ... }` 块(Rust 1.79+)让我们可以重复非 Copy 的常量表达式 N 次。
data: [const { std::mem::MaybeUninit::uninit() }; N],
len: 0,
}
}
pub fn push(&mut self, value: T) -> Result<(), T> {
if self.len >= N {
return Err(value); // 缓冲区满 —— 将值返回给调用者
}
// SAFETY: len < N,所以 data[len] 在边界内。
// 我们将有效的 T 写入 MaybeUninit 槽中。
self.data[self.len] = std::mem::MaybeUninit::new(value);
self.len += 1;
Ok(())
}
pub fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
// SAFETY: index < len,且 data[0..len] 都已初始化。
Some(unsafe { self.data[index].assume_init_ref() })
} else {
None
}
}
}
impl<T, const N: usize> Drop for StackBuf<T, N> {
fn drop(&mut self) {
// SAFETY: data[0..len] 已初始化 —— 正确地 drop 它们。
for i in 0..self.len {
unsafe { self.data[i].assume_init_drop(); }
}
}
}
}
可靠 unsafe 代码的三条规则:
- 记录不变量 —— 每个
// SAFETY:注释解释为什么操作有效 - 封装 —— unsafe 在安全 API 内部;用户不会触发 UB
- 最小化 —— 只有最小的块是
unsafe
FFI 模式:从 Rust 调用 C
#![allow(unused)]
fn main() {
// 声明 C 函数签名:
extern "C" {
fn strlen(s: *const std::ffi::c_char) -> usize;
fn printf(format: *const std::ffi::c_char, ...) -> std::ffi::c_int;
}
// 安全包装器:
fn safe_strlen(s: &str) -> usize {
let c_string = std::ffi::CString::new(s).expect("string contains null byte");
// SAFETY: c_string 是有效的空终止字符串,在调用期间存活。
unsafe { strlen(c_string.as_ptr()) }
}
// 从 C 调用 Rust(导出函数):
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
}
常见 FFI 类型:
| Rust | C | 说明 |
|---|---|---|
i32 / u32 | int32_t / uint32_t | 固定宽度,安全 |
*const T / *mut T | const T* / T* | 原始指针 |
std::ffi::CStr | const char* (借用) | 空终止,借用 |
std::ffi::CString | char* (拥有) | 空终止,拥有 |
std::ffi::c_void | void | 不透明指针目标 |
Option<fn(...)> | 可空函数指针 | None = NULL |
常见 UB 陷阱
| 陷阱 | 示例 | 为什么是 UB |
|---|---|---|
| 空指针解引用 | *std::ptr::null::<i32>() | 解引用空指针总是 UB |
| 悬空指针 | drop() 后解引用 | 内存可能被重用 |
| 数据竞争 | 两个线程写入 static mut | 无同步的并发写入 |
错误的 assume_init | MaybeUninit::<String>::uninit().assume_init() | 读取未初始化内存。注意:[const { MaybeUninit::uninit() }; N] (Rust 1.79+) 是创建 MaybeUninit 数组的安全方法 —— 不需要 unsafe 或 assume_init(参见上面的 StackBuf::new()) |
| 违反别名 | 创建两个 &mut 指向同一数据 | 违反 Rust 的别名模型 |
| 无效枚举值 | std::mem::transmute::<u8, bool>(2) | bool 只能是 0 或 1 |
何时在生产环境中使用
unsafe:
- FFI 边界(调用 C/C++ 代码)
- 性能关键的内部循环(避免边界检查)
- 构建原语(
Vec、HashMap—— 这些在内部使用 unsafe)- 如果可以避免,绝不要在应用程序逻辑中使用
自定义分配器 —— Arena 和 Slab 模式
在 C 中,你会为特定的分配模式编写自定义 malloc() 替代品 ——
一次性释放所有内容的 arena 分配器、用于固定大小对象的 slab 分配器、
或用于高吞吐量系统的池分配器。Rust 通过
GlobalAlloc trait 和分配器 crate 提供相同的能力,同时受益于生命周期作用域的
arena,在编译时防止释放后使用。
Arena 分配器 —— 批量分配、批量释放
Arena 通过向前推进指针来分配。单个物品不能被释放 —— 整个 arena 一次性释放。这非常适合请求作用域或 帧作用域的分配:
#![allow(unused)]
fn main() {
use bumpalo::Bump;
fn process_sensor_frame(raw_data: &[u8]) {
// 为这个帧的分配创建一个 arena
let arena = Bump::new();
// 在 arena 中分配对象 —— 每个约 2ns(只是指针推进)
let header = arena.alloc(parse_header(raw_data));
let readings: &mut [f32] = arena.alloc_slice_fill_default(header.sensor_count);
for (i, chunk) in raw_data[header.payload_offset..].chunks(4).enumerate() {
if i < readings.len() {
readings[i] = f32::from_le_bytes(chunk.try_into().unwrap());
}
}
// 使用 readings...
let avg = readings.iter().sum::<f32>() / readings.len() as f32;
println!("Frame avg: {avg:.2}");
// `arena` 在这里 drop —— 所有分配一次性释放 O(1)
// 无每个对象的析构函数开销,无碎片
}
fn parse_header(_: &[u8]) -> Header { Header { sensor_count: 4, payload_offset: 8 } }
struct Header { sensor_count: usize, payload_offset: usize }
}
Arena vs 标准分配器:
| 方面 | Vec::new() / Box::new() | Bump arena |
|---|---|---|
| 分配速度 | ~25ns (malloc) | ~2ns (指针推进) |
| 释放速度 | 每个对象的析构函数 | O(1) 批量释放 |
| 碎片 | 是(长生命周期进程) | arena 内无碎片 |
| 生命周期安全 | 堆 —— 在 Drop 时释放 | arena 引用 —— 编译时作用域 |
| 使用场景 | 通用 | 请求/帧/批处理 |
typed-arena —— 类型安全的 Arena
当所有 arena 对象都是同一类型时,typed-arena 提供更简单的 API,
返回带有 arena 生命周期的引用:
#![allow(unused)]
fn main() {
use typed_arena::Arena;
struct AstNode<'a> {
value: i32,
children: Vec<&'a AstNode<'a>>,
}
fn build_tree() {
let arena: Arena<AstNode<'_>> = Arena::new();
// 分配节点 —— 返回与 arena 生命周期绑定的 &AstNode
let root = arena.alloc(AstNode { value: 1, children: vec![] });
let left = arena.alloc(AstNode { value: 2, children: vec![] });
let right = arena.alloc(AstNode { value: 3, children: vec![] });
// 构建树 —— 所有引用在 `arena` 存活期间都有效
//(真正可变的树需要内部可变性来进行可变访问)
println!("Root: {}, Left: {}, Right: {}", root.value, left.value, right.value);
// `arena` 在这里 drop —— 所有节点一次性释放
}
}
Slab 分配器 —— 固定大小对象池
slab 分配器预先分配固定大小槽位的池。对象单独分配 和返回,但所有槽位大小相同 —— 消除 碎片并实现 O(1) 分配/释放:
#![allow(unused)]
fn main() {
use slab::Slab;
struct Connection {
id: u64,
buffer: [u8; 1024],
active: bool,
}
fn connection_pool_example() {
// 为连接预分配一个 slab
let mut connections: Slab<Connection> = Slab::with_capacity(256);
// insert 返回键(usize 索引)—— O(1)
let key1 = connections.insert(Connection {
id: 1001,
buffer: [0; 1024],
active: true,
});
let key2 = connections.insert(Connection {
id: 1002,
buffer: [0; 1024],
active: true,
});
// 按键访问 —— O(1)
if let Some(conn) = connections.get_mut(key1) {
conn.buffer[0..5].copy_from_slice(b"hello");
}
// remove 返回值 —— O(1),槽位被重用于下一次 insert
let removed = connections.remove(key2);
assert_eq!(removed.id, 1002);
// 下一次 insert 重用已释放的槽位 —— 无碎片
let key3 = connections.insert(Connection {
id: 1003,
buffer: [0; 1024],
active: true,
});
assert_eq!(key3, key2); // 重用同一槽位!
}
}
实现最小 Arena(用于 no_std)
对于裸机环境(无法引入 bumpalo),这是一个基于 unsafe 的最小 arena:
#![allow(unused)]
#![cfg_attr(not(test), no_std)]
fn main() {
use core::alloc::Layout;
use core::cell::{Cell, UnsafeCell};
/// 由固定大小字节数组支持的简单 bump 分配器。
/// 非线程安全 —— 在多线程上下文中每核心使用或加锁。
///
/// **重要**:像 `bumpalo` 一样,这个 arena 在 drop 时**不会**调用
/// 已分配物品的析构函数。带有 `Drop` 实现的类型会
/// 泄漏其资源(文件句柄、套接字等)。只分配
/// 没有有意义 `Drop` 实现的类型,或者在 arena 之前手动 drop 它们。
pub struct FixedArena<const N: usize> {
// UnsafeCell 在这里是必需的:我们通过 `&self` 修改 `buf`。
// 没有 UnsafeCell,将 &self.buf 转换为 *mut u8 会是 UB
//(违反 Rust 的别名模型 —— 共享引用意味着不可变)。
buf: UnsafeCell<[u8; N]>,
offset: Cell<usize>, // 用于 &self 分配的内部可变性
}
impl<const N: usize> FixedArena<N> {
pub const fn new() -> Self {
FixedArena {
buf: UnsafeCell::new([0; N]),
offset: Cell::new(0),
}
}
/// 在 arena 中分配一个 `T`。如果空间不足返回 `None`。
pub fn alloc<T>(&self, value: T) -> Option<&mut T> {
let layout = Layout::new::<T>();
let current = self.offset.get();
// 向上对齐
let aligned = (current + layout.align() - 1) & !(layout.align() - 1);
let new_offset = aligned + layout.size();
if new_offset > N {
return None; // Arena 满
}
self.offset.set(new_offset);
// SAFETY:
// - `aligned` 在 `buf` 边界内(上面已检查)
// - 对齐正确(对齐到 T 的要求)
// - 无别名:每个分配返回唯一的、不重叠的区域
// - UnsafeCell 授予通过 &self 修改的权限
// - arena 的生命周期比返回的引用长(调用者必须确保)
let ptr = unsafe {
let base = (self.buf.get() as *mut u8).add(aligned);
let typed = base as *mut T;
typed.write(value);
&mut *typed
};
Some(ptr)
}
/// 重置 arena —— 使所有之前的分配失效。
///
/// # Safety
/// 调用者必须确保不存在对 arena 分配数据的引用。
pub unsafe fn reset(&self) {
self.offset.set(0);
}
pub fn used(&self) -> usize {
self.offset.get()
}
pub fn remaining(&self) -> usize {
N - self.offset.get()
}
}
}
选择分配器策略
注意:下面的图表使用 Mermaid 语法。它在 GitHub 和支持 Mermaid 的工具中渲染 (带有
mermaid插件的 mdBook、带有 Mermaid 扩展的 VS Code)。在纯 Markdown 查看器中,你将看到原始源码。
graph TD
A["你的分配模式是什么?"] --> B{都是同一类型?}
A --> I{"环境?"}
B -->|是 | C{需要单独释放?}
B -->|否 | D{需要单独释放?}
C -->|是 | E["<b>Slab</b><br/>slab crate<br/>O(1) 分配 + 释放<br/>基于索引的访问"]
C -->|否 | F["<b>typed-arena</b><br/>批量分配,批量释放<br/>生命周期作用域引用"]
D -->|是 | G["<b>标准分配器</b><br/>Box, Vec 等。<br/>通用 malloc"]
D -->|否 | H["<b>Bump arena</b><br/>bumpalo crate<br/>~2ns 分配,O(1) 批量释放"]
I -->|no_std| J["FixedArena (自定义)<br/>或 embedded-alloc"]
I -->|std| K["bumpalo / typed-arena / slab"]
style E fill:#91e5a3,color:#000
style F fill:#91e5a3,color:#000
style G fill:#89CFF0,color:#000
style H fill:#91e5a3,color:#000
style J fill:#ffa07a,color:#000
style K fill:#91e5a3,color:#000
| C 模式 | Rust 等价物 | 关键优势 |
|---|---|---|
自定义 malloc() 池 | #[global_allocator] 实现 | 类型安全、可调试 |
obstack (GNU) | bumpalo::Bump | 生命周期作用域、无释放后使用 |
Kernel slab (kmem_cache) | slab::Slab<T> | 类型安全、基于索引 |
| 栈分配临时缓冲区 | FixedArena<N>(上面) | 无堆、const 可构造 |
alloca() | [T; N] 或 SmallVec | 编译时大小、无 UB |
交叉引用:对于裸机分配器设置(使用
embedded-alloc的#[global_allocator]), 请参阅 Rust Training for C Programmers 第 15.1 章 “Global Allocator Setup”, 其中涵盖嵌入式特定的引导。
关键要点 —— Unsafe Rust
- 记录不变量(
SAFETY:注释),封装在安全 API 后,最小化 unsafe 范围[const { MaybeUninit::uninit() }; N](Rust 1.79+) 替代旧的assume_init反模式- FFI 需要
extern "C"、#[repr(C)],以及小心的 null/生命周期处理- Arena 和 slab 分配器以通用灵活性换取分配速度
另见: 第 4 章 — PhantomData 了解方差和 drop-check 与 unsafe 代码的交互。第 8 章 — 智能指针 了解 Pin 和自引用类型。
练习:unsafe 的安全包装器 ★★★(约 45 分钟)
编写一个 FixedVec<T, const N: usize> —— 固定容量、栈分配的向量。
要求:
push(&mut self, value: T) -> Result<(), T>满时返回Err(value)pop(&mut self) -> Option<T>返回并移除最后一个元素as_slice(&self) -> &[T]借用已初始化的元素- 所有公共方法必须是安全的;所有 unsafe 必须用
SAFETY:注释封装 Drop必须清理已初始化的元素
🔑 解答
use std::mem::MaybeUninit;
pub struct FixedVec<T, const N: usize> {
data: [MaybeUninit<T>; N],
len: usize,
}
impl<T, const N: usize> FixedVec<T, N> {
pub fn new() -> Self {
FixedVec {
data: [const { MaybeUninit::uninit() }; N],
len: 0,
}
}
pub fn push(&mut self, value: T) -> Result<(), T> {
if self.len >= N { return Err(value); }
// SAFETY: len < N,所以 data[len] 在边界内。
self.data[self.len] = MaybeUninit::new(value);
self.len += 1;
Ok(())
}
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 { return None; }
self.len -= 1;
// SAFETY: data[len] 已初始化(递减前 len > 0)。
Some(unsafe { self.data[self.len].assume_init_read() })
}
pub fn as_slice(&self) -> &[T] {
// SAFETY: data[0..len] 都已初始化,且 MaybeUninit<T>
// 与 T 具有相同的布局。
unsafe { std::slice::from_raw_parts(self.data.as_ptr() as *const T, self.len) }
}
pub fn len(&self) -> usize { self.len }
pub fn is_empty(&self) -> bool { self.len == 0 }
}
impl<T, const N: usize> Drop for FixedVec<T, N> {
fn drop(&mut self) {
// SAFETY: data[0..len] 已初始化 —— drop 每一个。
for i in 0..self.len {
unsafe { self.data[i].assume_init_drop(); }
}
}
}
fn main() {
let mut v = FixedVec::<String, 4>::new();
v.push("hello".into()).unwrap();
v.push("world".into()).unwrap();
assert_eq!(v.as_slice(), &["hello", "world"]);
assert_eq!(v.pop(), Some("world".into()));
assert_eq!(v.len(), 1);
}
13. Macros — Code That Writes Code 🟡
你将学到:
- 带有模式匹配和重复的声明式宏(
macro_rules!)- 何时宏是正确的工具 vs 泛型/traits
- 过程宏:derive、属性和函数式
- 使用
syn和quote编写自定义 derive 宏
声明式宏(macro_rules!)
宏在编译时匹配语法模式并展开为代码:
#![allow(unused)]
fn main() {
// 创建一个 HashMap 的简单宏
macro_rules! hashmap {
// 匹配:key => value 对,用逗号分隔
( $( $key:expr => $value:expr ),* $(,)? ) => {
{
let mut map = std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}
};
}
let scores = hashmap! {
"Alice" => 95,
"Bob" => 87,
"Carol" => 92,
};
// 展开为:
// let mut map = HashMap::new();
// map.insert("Alice", 95);
// map.insert("Bob", 87);
// map.insert("Carol", 92);
// map
}
宏片段类型:
| 片段 | 匹配 | 示例 |
|---|---|---|
$x:expr | 任何表达式 | 42, a + b, foo() |
$x:ty | 类型 | i32, Vec<String> |
$x:ident | 标识符 | my_var, Config |
$x:pat | 模式 | Some(x), _ |
$x:stmt | 语句 | let x = 5; |
$x:tt | 单个 token 树 | 任何内容(最灵活) |
$x:literal | 字面值 | 42, "hello", true |
重复:$( ... ),* 表示“零个或多个,逗号分隔“
#![allow(unused)]
fn main() {
// 自动生成测试函数
macro_rules! test_cases {
( $( $name:ident: $input:expr => $expected:expr ),* $(,)? ) => {
$(
#[test]
fn $name() {
assert_eq!(process($input), $expected);
}
)*
};
}
test_cases! {
test_empty: "" => "",
test_hello: "hello" => "HELLO",
test_trim: " spaces " => "SPACES",
}
// 生成三个独立的 #[test] 函数
}
何时(不)使用宏
使用宏当:
- 减少 traits/泛型无法处理的样板代码(可变参数、DRY 测试生成)
- 创建 DSL(
html!,sql!,vec!) - 条件代码生成(
cfg!,compile_error!)
不使用宏当:
- 函数或泛型可以工作(宏更难调试,自动补全无法帮助)
- 需要在宏内部进行类型检查(宏在 token 上操作,而非类型)
- 模式只使用一两次(不值得抽象成本)
#![allow(unused)]
fn main() {
// ❌ 不必要的宏 —— 函数就可以:
macro_rules! double {
($x:expr) => { $x * 2 };
}
// ✅ 直接用函数:
fn double(x: i32) -> i32 { x * 2 }
// ✅ 好的宏用法 —— 可变参数,不能是函数:
macro_rules! println {
($($arg:tt)*) => { /* format string + args */ };
}
}
过程宏概述
过程宏是转换 token 流的 Rust 函数。它们需要一个独立的 crate,设置 proc-macro = true:
#![allow(unused)]
fn main() {
// 三种类型的过程宏:
// 1. Derive 宏 —— #[derive(MyTrait)]
// 从结构体定义生成 trait 实现
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config {
name: String,
port: u16,
}
// 2. 属性宏 —— #[my_attribute]
// 转换被注解的项
#[route(GET, "/api/users")]
async fn list_users() -> Json<Vec<User>> { /* ... */ }
// 3. 函数式宏 —— my_macro!(...)
// 自定义语法
let query = sql!(SELECT * FROM users WHERE id = ?);
}
Derive 宏实战
最常见的过程宏类型。下面是 #[derive(Debug)] 的概念工作原理:
#![allow(unused)]
fn main() {
// 输入(你的结构体):
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
// derive 宏生成:
impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
}
常用的 derive 宏:
| Derive | Crate | 生成什么 |
|---|---|---|
Debug | std | fmt::Debug 实现(调试打印) |
Clone, Copy | std | 值复制 |
PartialEq, Eq | std | 相等性比较 |
Hash | std | 用于 HashMap 键的哈希 |
Serialize, Deserialize | serde | JSON/YAML 等编码 |
Error | thiserror | std::error::Error + Display |
Parser | clap | CLI 参数解析 |
Builder | derive_builder | Builder 模式 |
实用建议:大胆使用 derive 宏 —— 它们消除容易出错的样板代码。 编写自己的过程宏是高级主题;在构建自定义宏之前先使用现有的 (
serde、thiserror、clap)。
宏卫生和 $crate
卫生意味着宏内部创建的标识符不会与调用者作用域中的标识符冲突。
Rust 的 macro_rules! 是部分卫生的:
macro_rules! make_var {
() => {
let x = 42; // 这个 'x' 在宏的作用域中
};
}
fn main() {
let x = 10;
make_var!(); // 创建不同的 'x'(卫生的)
println!("{x}"); // 打印 10,不是 42 —— 宏的 x 不会泄漏
}
$crate:在库中编写宏时,使用 $crate 引用你自己的 crate ——
无论用户如何导入你的 crate,它都能正确解析:
#![allow(unused)]
fn main() {
// 在 my_diagnostics crate 中:
pub fn log_result(msg: &str) {
println!("[diag] {msg}");
}
#[macro_export]
macro_rules! diag_log {
($($arg:tt)*) => {
// ✅ $crate 总是解析为 my_diagnostics,即使用户
// 在他们的 Cargo.toml 中重命名了 crate
$crate::log_result(&format!($($arg)*))
};
}
// ❌ 不使用 $crate:
// my_diagnostics::log_result(...) ← 如果用户这样写会失败:
// [dependencies]
// diag = { package = "my_diagnostics", version = "1" }
}
规则:在
#[macro_export]宏中总是使用$crate::。绝不要直接使用 你的 crate 名称。
递归宏和 tt Munching
递归宏一次处理一个 token 的输入 —— 这种技术称为
tt munching(token-tree munching):
// 计算传递给宏的表达式数量
macro_rules! count {
// 基本情况:没有 token 剩余
() => { 0usize };
// 递归情况:消费一个表达式,计算其余的
($head:expr $(, $tail:expr)* $(,)?) => {
1usize + count!($($tail),*)
};
}
fn main() {
let n = count!("a", "b", "c", "d");
assert_eq!(n, 4);
// 在编译时也有效:
const N: usize = count!(1, 2, 3);
assert_eq!(N, 3);
}
#![allow(unused)]
fn main() {
// 从表达式列表构建异构元组:
macro_rules! tuple_from {
// 基本:单个元素
($single:expr $(,)?) => { ($single,) };
// 递归:第一个元素 + 其余
($head:expr, $($tail:expr),+ $(,)?) => {
($head, tuple_from!($($tail),+))
};
}
let t = tuple_from!(1, "hello", 3.14, true);
// 展开为:(1, ("hello", (3.14, (true,))))
}
片段说明符的微妙之处:
| 片段 | 陷阱 |
|---|---|
$x:expr | 贪婪解析 —— 1 + 2 是一个表达式,不是三个 token |
$x:ty | 贪婪解析 —— Vec<String> 是一个类型;后面不能跟 + 或 < |
$x:tt | 精确匹配一个 token 树 —— 最灵活,检查最少 |
$x:ident | 仅限普通标识符 —— 不是路径如 std::io |
$x:pat | 在 Rust 2021 中,匹配 `A |
何时使用
tt:当你需要转发 token 给另一个宏而不受解析器约束时。$($args:tt)*是“接受一切“模式(println!、format!、vec!使用)。
使用 syn 和 quote 编写 Derive 宏
Derive 宏存在于独立的 crate 中(proc-macro = true),使用 syn(解析 Rust)
和 quote(生成 Rust)转换 token 流:
# my_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
#![allow(unused)]
fn main() {
// my_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// 生成 `describe()` 方法的 derive 宏
/// 返回结构体名称和字段名称。
#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let name_str = name.to_string();
// 提取字段名称(仅适用于命名字段的结构体)
let fields = match &input.data {
syn::Data::Struct(data) => {
data.fields.iter()
.filter_map(|f| f.ident.as_ref())
.map(|id| id.to_string())
.collect::<Vec<_>>()
}
_ => vec![],
};
let field_list = fields.join(", ");
let expanded = quote! {
impl #name {
pub fn describe() -> String {
format!("{} {{ {} }}", #name_str, #field_list)
}
}
};
TokenStream::from(expanded)
}
}
// 在应用程序 crate 中:
use my_derive::Describe;
#[derive(Describe)]
struct SensorReading {
sensor_id: u16,
value: f64,
timestamp: u64,
}
fn main() {
println!("{}", SensorReading::describe());
// "SensorReading { sensor_id, value, timestamp }"
}
工作流:TokenStream(原始 token)→ syn::parse(AST)→
检查/转换 → quote!(生成 token)→ TokenStream(返回编译器)。
| Crate | 角色 | 关键类型 |
|---|---|---|
proc-macro | 编译器接口 | TokenStream |
syn | 将 Rust 源码解析为 AST | DeriveInput, ItemFn, Type |
quote | 从模板生成 Rust token | quote!{}, #variable 插值 |
proc-macro2 | syn/quote 和 proc-macro 之间的桥接 | TokenStream, Span |
实用技巧:在编写自己的宏之前,先研究简单 derive 宏的源码, 如
thiserror或derive_more。cargo expand命令(通过cargo-expand) 显示任何宏的展开结果 —— 对调试非常宝贵。
关键要点 —— 宏
macro_rules!用于简单代码生成;过程宏(syn+quote)用于复杂 derive- 可能时优先使用泛型/traits 而非宏 —— 宏更难调试和维护
$crate确保卫生;ttmunching 实现递归模式匹配
另见: 第 2 章 — Traits 了解何时 traits/泛型优于宏。第 13 章 — 测试 了解测试宏生成的代码。
flowchart LR
A["Source code"] --> B["macro_rules!<br>pattern matching"]
A --> C["#[derive(MyMacro)]<br>proc macro"]
B --> D["Token expansion"]
C --> E["syn: parse AST"]
E --> F["Transform"]
F --> G["quote!: generate tokens"]
G --> D
D --> H["Compiled code"]
style A fill:#e8f4f8,stroke:#2980b9,color:#000
style B fill:#d4efdf,stroke:#27ae60,color:#000
style C fill:#fdebd0,stroke:#e67e22,color:#000
style D fill:#fef9e7,stroke:#f1c40f,color:#000
style E fill:#fdebd0,stroke:#e67e22,color:#000
style F fill:#fdebd0,stroke:#e67e22,color:#000
style G fill:#fdebd0,stroke:#e67e22,color:#000
style H fill:#d4efdf,stroke:#27ae60,color:#000
练习:声明式宏 —— map! ★(约 15 分钟)
编写一个 map! 宏,从键值对创建 HashMap:
let m = map! {
"host" => "localhost",
"port" => "8080",
};
assert_eq!(m.get("host"), Some(&"localhost"));
要求:支持尾随逗号和空调用 map!{}。
🔑 解答
macro_rules! map {
() => { std::collections::HashMap::new() };
( $( $key:expr => $val:expr ),+ $(,)? ) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($key, $val); )+
m
}};
}
fn main() {
let config = map! {
"host" => "localhost",
"port" => "8080",
"timeout" => "30",
};
assert_eq!(config.len(), 3);
assert_eq!(config["host"], "localhost");
let empty: std::collections::HashMap<String, String> = map!();
assert!(empty.is_empty());
let scores = map! { 1 => 100, 2 => 200 };
assert_eq!(scores[&1], 100);
}
14. Testing and Benchmarking Patterns 🟢
你将学到:
- Rust 的三层测试:单元测试、集成测试和文档测试
- 使用 proptest 进行属性基测试以发现边界情况
- 使用 criterion 进行可靠的性能测量
- 无需重量级框架的 mocking 策略
单元测试、集成测试、文档测试
Rust 有三层内置测试:
#![allow(unused)]
fn main() {
// --- 单元测试:与代码在同一文件中 ---
pub fn factorial(n: u64) -> u64 {
(1..=n).product()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_factorial_zero() {
// (1..=0).product() 返回 1 —— 空范围的乘法单位元
assert_eq!(factorial(0), 1);
}
#[test]
fn test_factorial_five() {
assert_eq!(factorial(5), 120);
}
#[test]
#[cfg(debug_assertions)] // 溢出检查仅在 debug 模式下启用
#[should_panic(expected = "overflow")]
fn test_factorial_overflow() {
// ⚠️ 此测试仅在 debug 模式下通过(启用溢出检查)。
// 在 release 模式下(`cargo test --release`),u64 算术静默绕回
// 不会发生 panic。使用 `checked_mul` 或
// `overflow-checks = true` 配置以获得 release 模式安全性。
factorial(100); // 应该在溢出时 panic
}
#[test]
fn test_with_result() -> Result<(), Box<dyn std::error::Error>> {
// 测试可以返回 Result —— ? 可以在内部使用!
let value: u64 = "42".parse()?;
assert_eq!(value, 42);
Ok(())
}
}
}
#![allow(unused)]
fn main() {
// --- 集成测试:在 tests/ 目录中 ---
// tests/integration_test.rs
// 这些只测试 crate 的公共 API
use my_crate::factorial;
#[test]
fn test_factorial_from_outside() {
assert_eq!(factorial(10), 3_628_800);
}
}
#![allow(unused)]
fn main() {
// --- 文档测试:在文档注释中 ---
/// 计算 `n` 的阶乘。
///
/// # Examples
///
/// ```
/// use my_crate::factorial;
/// assert_eq!(factorial(5), 120);
/// ```
///
/// # Panics
///
/// 如果结果溢出 `u64` 则 panic。
///
/// ```should_panic
/// my_crate::factorial(100);
/// ```
pub fn factorial(n: u64) -> u64 {
(1..=n).product()
}
// 文档测试由 `cargo test` 编译和运行 —— 它们保持示例的真实性。
}
测试脚手架和设置
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
// 共享设置 —— 创建辅助函数
fn setup_database() -> TestDb {
let db = TestDb::new_in_memory();
db.run_migrations();
db.seed_test_data();
db
}
#[test]
fn test_user_creation() {
let db = setup_database();
let user = db.create_user("Alice", "alice@test.com").unwrap();
assert_eq!(user.name, "Alice");
}
#[test]
fn test_user_deletion() {
let db = setup_database();
db.create_user("Bob", "bob@test.com").unwrap();
assert!(db.delete_user("Bob").is_ok());
assert!(db.get_user("Bob").is_none());
}
// 使用 Drop 清理(RAII):
struct TempDir {
path: std::path::PathBuf,
}
impl TempDir {
fn new() -> Self {
// Cargo.toml: rand = "0.8"
let path = std::env::temp_dir().join(format!("test_{}", rand::random::<u32>()));
std::fs::create_dir_all(&path).unwrap();
TempDir { path }
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
#[test]
fn test_file_operations() {
let dir = TempDir::new(); // 创建
std::fs::write(dir.path.join("test.txt"), "hello").unwrap();
assert!(dir.path.join("test.txt").exists());
} // dir 在这里 drop —— 临时目录被清理
}
}
属性基测试(proptest)
不测试特定值,而是测试应该始终成立的属性:
#![allow(unused)]
fn main() {
// Cargo.toml: proptest = "1"
use proptest::prelude::*;
fn reverse(v: &[i32]) -> Vec<i32> {
v.iter().rev().cloned().collect()
}
proptest! {
#[test]
fn test_reverse_twice_is_identity(v in prop::collection::vec(any::<i32>(), 0..100)) {
// 属性:反转两次返回原始值
assert_eq!(reverse(&reverse(&v)), v);
}
#[test]
fn test_reverse_preserves_length(v in prop::collection::vec(any::<i32>(), 0..100)) {
assert_eq!(reverse(&v).len(), v.len());
}
#[test]
fn test_sort_is_idempotent(mut v in prop::collection::vec(any::<i32>(), 0..100)) {
v.sort();
let sorted_once = v.clone();
v.sort();
assert_eq!(v, sorted_once); // 排序两次 = 排序一次
}
#[test]
fn test_parse_roundtrip(x in any::<f64>().prop_filter("finite", |x| x.is_finite())) {
// 属性:格式化然后解析返回相同的值
let s = format!("{x}");
let parsed: f64 = s.parse().unwrap();
prop_assert!((x - parsed).abs() < f64::EPSILON);
}
}
}
何时使用 proptest:当你测试具有 large 输入空间的函数并希望确信它 能处理你想不到的边界情况时。proptest 生成数百个随机输入, 并将失败缩小到最小的可复现案例。
使用 criterion 进行基准测试
#![allow(unused)]
fn main() {
// Cargo.toml:
// [dev-dependencies]
// criterion = { version = "0.5", features = ["html_reports"] }
//
// [[bench]]
// name = "my_benchmarks"
// harness = false
// benches/my_benchmarks.rs
use criterion::{criterion_group, criterion_main, Criterion, black_box};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fibonacci 20", |b| {
b.iter(|| fibonacci(black_box(20)))
});
// 比较不同的实现:
let mut group = c.benchmark_group("fibonacci_compare");
for size in [10, 15, 20, 25] {
group.bench_with_input(
criterion::BenchmarkId::from_parameter(size),
&size,
|b, &size| b.iter(|| fibonacci(black_box(size))),
);
}
group.finish();
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
// 运行:cargo bench
// 在 target/criterion/ 中生成 HTML 报告
}
无需框架的 Mocking 策略
Rust 的 trait 系统提供自然的依赖注入 —— 不需要 mocking 框架:
#![allow(unused)]
fn main() {
// 将行为定义为 trait
trait Clock {
fn now(&self) -> std::time::Instant;
}
trait HttpClient {
fn get(&self, url: &str) -> Result<String, String>;
}
// 生产环境实现
struct RealClock;
impl Clock for RealClock {
fn now(&self) -> std::time::Instant { std::time::Instant::now() }
}
// 服务依赖抽象
struct CacheService<C: Clock, H: HttpClient> {
clock: C,
client: H,
ttl: std::time::Duration,
}
impl<C: Clock, H: HttpClient> CacheService<C, H> {
fn fetch(&self, url: &str) -> Result<String, String> {
// 使用 self.clock 和 self.client —— 可注入
self.client.get(url)
}
}
// 使用 mock 实现测试 —— 不需要框架!
#[cfg(test)]
mod tests {
use super::*;
struct MockClock {
fixed_time: std::time::Instant,
}
impl Clock for MockClock {
fn now(&self) -> std::time::Instant { self.fixed_time }
}
struct MockHttpClient {
response: String,
}
impl HttpClient for MockHttpClient {
fn get(&self, _url: &str) -> Result<String, String> {
Ok(self.response.clone())
}
}
#[test]
fn test_cache_service() {
let service = CacheService {
clock: MockClock { fixed_time: std::time::Instant::now() },
client: MockHttpClient { response: "cached data".into() },
ttl: std::time::Duration::from_secs(300),
};
assert_eq!(service.fetch("http://example.com").unwrap(), "cached data");
}
}
}
测试哲学:在集成测试中优先使用真实的依赖,在单元测试中使用基于 trait 的 mock。 除非你的依赖图很复杂,否则避免 mocking 框架 —— Rust 的 trait 泛型自然地处理大多数情况。
关键要点 —— 测试
- 文档测试(
///)既是文档也是回归测试 —— 它们被编译和运行proptest生成随机输入以发现你永远不会手动编写的边界情况criterion提供具有 HTML 报告的统计上严格的基准测试- 通过 trait 泛型 + 测试双重 mock,而非 mock 框架
另见: 第 12 章 — 宏 了解测试宏生成的代码。第 14 章 — API 设计 了解模块布局如何影响测试组织。
练习:使用 proptest 进行属性基测试 ★★(约 25 分钟)
编写一个 SortedVec<T: Ord> 包装器,维护排序不变量。使用 proptest 验证:
- 在任何插入序列之后,内部 vec 始终排序
contains()与 stdlib 的Vec::contains()一致- 长度等于插入次数
🔑 解答
#[derive(Debug)]
struct SortedVec<T: Ord> {
inner: Vec<T>,
}
impl<T: Ord> SortedVec<T> {
fn new() -> Self { SortedVec { inner: Vec::new() } }
fn insert(&mut self, value: T) {
let pos = self.inner.binary_search(&value).unwrap_or_else(|p| p);
self.inner.insert(pos, value);
}
fn contains(&self, value: &T) -> bool {
self.inner.binary_search(value).is_ok()
}
fn len(&self) -> usize { self.inner.len() }
fn as_slice(&self) -> &[T] { &self.inner }
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn always_sorted(values in proptest::collection::vec(-1000i32..1000, 0..100)) {
let mut sv = SortedVec::new();
for v in &values {
sv.insert(*v);
}
for w in sv.as_slice().windows(2) {
prop_assert!(w[0] <= w[1]);
}
prop_assert_eq!(sv.len(), values.len());
}
#[test]
fn contains_matches_stdlib(values in proptest::collection::vec(0i32..50, 1..30)) {
let mut sv = SortedVec::new();
for v in &values {
sv.insert(*v);
}
for v in &values {
prop_assert!(sv.contains(v));
}
prop_assert!(!sv.contains(&9999));
}
}
}
15. Crate 架构和 API 设计 🟡
你将学到:
- 模块布局约定和重新导出策略
- 精制 crate 的公共 API 设计清单
- 人体工学的参数模式:
impl Into、AsRef、Cow- 使用
TryFrom和已验证类型的“解析,不要验证“- 特性标志、条件编译和工作区组织
模块布局约定
my_crate/
├── Cargo.toml
├── src/
│ ├── lib.rs # Crate 根 —— 重新导出和公共 API
│ ├── config.rs # 特性模块
│ ├── parser/ # 带有子模块的复杂模块
│ │ ├── mod.rs # 或父级的 parser.rs(Rust 2018+)
│ │ ├── lexer.rs
│ │ └── ast.rs
│ ├── error.rs # 错误类型
│ └── utils.rs # 内部辅助(pub(crate))
├── tests/
│ └── integration.rs # 集成测试
├── benches/
│ └── perf.rs # 基准测试
└── examples/
└── basic.rs # cargo run --example basic
#![allow(unused)]
fn main() {
// lib.rs —— 使用重新导出策划你的公共 API:
mod config;
mod error;
mod parser;
mod utils;
// 重新导出用户需要的内容:
pub use config::Config;
pub use error::Error;
pub use parser::Parser;
// 公共类型在 crate 根 —— 用户编写:
// use my_crate::Config;
// 而不是:use my_crate::config::Config;
}
可见性修饰符:
| 修饰符 | 对谁可见 |
|---|---|
pub | 所有人 |
pub(crate) | 仅这个 crate |
pub(super) | 父模块 |
pub(in path) | 特定的祖先模块 |
| (无) | 当前模块及其子模块 |
公共 API 设计清单
- 接受引用,返回 owned ——
fn process(input: &str) -> String - 对参数使用
impl Trait——fn read(r: impl Read)而不是fn read<R: Read>(r: R),签名更清晰 - 返回
Result,而不是panic!—— 让调用者决定如何处理错误 - 实现标准 trait ——
Debug、Display、Clone、Default、From/Into - 使无效状态不可表示 —— 使用类型状态和新类型
- 对复杂配置使用 builder 模式 —— 如果字段是必需的则使用类型状态
- 密封你不想让用户实现的 trait ——
pub trait Sealed: private::Sealed {} - 标记类型和函数为
#[must_use]—— 防止静默丢弃重要的Result、守卫或值。适用于任何忽略返回值几乎肯定是 bug 的类型:#![allow(unused)] fn main() { #[must_use = "丢弃守卫会立即释放锁"] pub struct LockGuard<'a, T> { /* ... */ } #[must_use] pub fn validate(input: &str) -> Result<ValidInput, ValidationError> { /* ... */ } }
#![allow(unused)]
fn main() {
// 密封 trait 模式 —— 用户可以使用但不能实现:
mod private {
pub trait Sealed {}
}
pub trait DatabaseDriver: private::Sealed {
fn connect(&self, url: &str) -> Connection;
}
// 只有这个 crate 中的类型可以实现 Sealed → 只有我们可以实现 DatabaseDriver
pub struct PostgresDriver;
impl private::Sealed for PostgresDriver {}
impl DatabaseDriver for PostgresDriver {
fn connect(&self, url: &str) -> Connection { /* ... */ }
}
}
#[non_exhaustive]—— 标记公共枚举和结构体,这样添加变体或字段不是破坏性变更。下游 crate 必须在 match 语句中使用通配符臂(_ =>),并且不能使用结构体字面量语法构造类型:#![allow(unused)] fn main() { #[non_exhaustive] pub enum DiagError { Timeout, HardwareFault, // 在未来的版本中添加新变体不是语义化版本破坏。 } }
人体工学参数模式 —— impl Into、AsRef、Cow
最有影响力的 Rust API 模式之一是在函数参数中接受最通用的类型,这样调用者不需要在每个调用点重复 .to_string()、&*s 或 .as_ref()。这是 Rust 特定版本的“接受时宽容“。
impl Into<T> —— 接受任何可转换的内容
#![allow(unused)]
fn main() {
// ❌ 摩擦:调用者必须手动转换
fn connect(host: String, port: u16) -> Connection {
// ...
}
connect("localhost".to_string(), 5432); // 讨厌的 .to_string()
connect(hostname.clone(), 5432); // 如果已经有 String 则不必要的 clone
// ✅ 人体工学:接受任何可以转换为 String 的内容
fn connect(host: impl Into<String>, port: u16) -> Connection {
let host = host.into(); // 在函数内部转换一次
// ...
}
connect("localhost", 5432); // &str —— 零摩擦
connect(hostname, 5432); // String —— move,无 clone
}
这有效是因为 Rust 的 From/Into trait 对提供通用转换。
当你接受 impl Into<T> 时,你是在说:“给我任何知道如何变成 T 的东西。”
AsRef<T> —— 借用为引用
AsRef<T> 是 Into<T> 的借用对应物。当你只需要读取数据而不需要所有权时使用它:
#![allow(unused)]
fn main() {
use std::path::Path;
// ❌ 强制调用者转换为 &Path
fn file_exists(path: &Path) -> bool {
path.exists()
}
file_exists(Path::new("/tmp/test.txt")); // 笨拙
// ✅ 接受任何可以表现为 &Path 的内容
fn file_exists(path: impl AsRef<Path>) -> bool {
path.as_ref().exists()
}
file_exists("/tmp/test.txt"); // &str ✅
file_exists(String::from("/tmp/test.txt")); // String ✅
file_exists(Path::new("/tmp/test.txt")); // &Path ✅
file_exists(PathBuf::from("/tmp/test.txt")); // PathBuf ✅
// 字符串参数的相同模式:
fn log_message(msg: impl AsRef<str>) {
println!("[LOG] {}", msg.as_ref());
}
log_message("hello"); // &str ✅
log_message(String::from("hello")); // String ✅
}
Cow<T> —— 写入时克隆
Cow<'a, T>(Clone on Write)延迟分配直到需要修改。
它持有借用的 &T 或 owned 的 T::Owned。当大多数调用不需要修改数据时这很完美:
#![allow(unused)]
fn main() {
use std::borrow::Cow;
/// 规范化诊断消息 —— 仅在需要更改时分配。
fn normalize_message(msg: &str) -> Cow<'_, str> {
if msg.contains('\t') || msg.contains('\r') {
// 必须分配 —— 我们需要修改内容
Cow::Owned(msg.replace('\t', " ").replace('\r', ""))
} else {
// 无分配 —— 只是借用原始内容
Cow::Borrowed(msg)
}
}
// 大多数消息无分配地通过:
let clean = normalize_message("All tests passed"); // 借用 —— 免费
let fixed = normalize_message("Error:\tfailed\r\n"); // Owned —— 分配
// Cow<str> 实现 Deref<Target=str>,所以它像 &str 一样工作:
println!("{}", clean);
println!("{}", fixed.to_uppercase());
}
快速参考:使用哪个
你需要在函数内部拥有数据的所有权吗?
├── 是 → impl Into<T>
│ "给我任何可以变成 T 的东西"
└── 否 → 你只需要读取它吗?
├── 是 → impl AsRef<T> 或 &T
│ "给我任何我可以借用为 &T 的东西"
└── 可能(有时可能需要修改?)
└── Cow<'_, T>
"可能时借用,必须时才克隆"
| 模式 | 所有权 | 分配 | 何时使用 |
|---|---|---|---|
&str | 借用 | 从不 | 简单字符串参数 |
impl AsRef<str> | 借用 | 从不 | 接受 String、&str 等 —— 只读 |
impl Into<String> | Owned | 转换时 | 接受 &str、String —— 将存储/拥有 |
Cow<'_, str> | 任一 | 仅在修改时 | 通常不修改的处理 |
&[u8] / impl AsRef<[u8]> | 借用 | 从不 | 字节导向的 API |
Borrow<T>vsAsRef<T>:两者都提供&T,但Borrow<T>额外保证Eq、Ord和Hash在原始形式和借用形式之间一致。 这就是为什么HashMap<String, V>::get()接受&Q where String: Borrow<Q>—— 而不是AsRef。 当借用形式用作查找键时使用Borrow;对一般“给我引用“参数使用AsRef。
在 API 中组合转换
#![allow(unused)]
fn main() {
/// 使用人体工学参数的精心设计诊断 API:
pub struct DiagRunner {
name: String,
config_path: PathBuf,
results: HashMap<String, TestResult>,
}
impl DiagRunner {
/// 对 name 接受任何字符串类型,对 config 接受任何路径类型。
pub fn new(
name: impl Into<String>,
config_path: impl Into<PathBuf>,
) -> Self {
DiagRunner {
name: name.into(),
config_path: config_path.into(),
}
}
/// 对只读查找接受任何 AsRef<str>。
pub fn get_result(&self, test_name: impl AsRef<str>) -> Option<&TestResult> {
self.results.get(test_name.as_ref())
}
}
// 所有这些都零摩擦地工作:
let runner = DiagRunner::new("GPU Diag", "/etc/diag_tool/config.json");
let runner = DiagRunner::new(format!("Diag-{}", node_id), config_path);
let runner = DiagRunner::new(name_string, path_buf);
}
案例研究:设计公共 Crate API —— 之前和之后
一个将字符串类型的内部 API 演化为人体工学、类型安全的公共 API 的真实示例。考虑一个配置解析 crate:
之前(字符串类型,容易误用):
#![allow(unused)]
fn main() {
// ❌ 所有参数都是字符串 —— 无编译时验证
pub fn parse_config(path: &str, format: &str, strict: bool) -> Result<Config, String> {
// 哪些格式是有效的?"json"?"JSON"?"Json"?
// path 是文件路径还是 URL?
// "strict" 到底是什么意思?
todo!()
}
}
之后(类型安全、自文档化):
#![allow(unused)]
fn main() {
use std::path::Path;
/// 支持的文件配置格式。
#[derive(Debug, Clone, Copy)]
#[non_exhaustive] // 添加格式不会破坏下游
pub enum Format {
Json,
Toml,
Yaml,
}
/// 控制解析严格性。
#[derive(Debug, Clone, Copy, Default)]
pub enum Strictness {
/// 拒绝未知字段(库的默认值)
#[default]
Strict,
/// 忽略未知字段(对前向兼容配置有用)
Lenient,
}
pub fn parse_config(
path: &Path, // 类型强制:必须是文件系统路径
format: Format, // 枚举:不可能传递无效格式
strictness: Strictness, // 命名替代,而不是裸 bool
) -> Result<Config, ConfigError> {
todo!()
}
}
改进了什么:
| 方面 | 之前 | 之后 |
|---|---|---|
| 格式验证 | 运行时字符串比较 | 编译时枚举 |
| 路径类型 | 裸 &str(可以是任何东西) | &Path(特定于文件系统) |
| 严格性 | 神秘 bool | 自文档化枚举 |
| 错误类型 | String(不透明) | ConfigError(结构化) |
| 可扩展性 | 破坏性变更 | #[non_exhaustive] |
经验法则:如果你发现自己编写对字符串值的
match,考虑用枚举替换参数。 如果参数是上下文中不明显的布尔值,使用双变体枚举代替。
解析不要验证 —— TryFrom 和已验证类型
“解析,不要验证“是一个原则,它说:不要检查数据然后传递未经检查的原始形式 —— 而是解析为只有在数据有效时才能存在的类型。
Rust 的 TryFrom trait 是此的标准工具。
问题:验证无强制
#![allow(unused)]
fn main() {
// ❌ 验证后使用:没有阻止在检查后使用无效值
fn process_port(port: u16) {
if port == 0 || port > 65535 {
panic!("Invalid port"); // 我们检查了,但是...
}
start_server(port); // 如果有人直接调用 start_server(0) 怎么办?
}
// ❌ 字符串类型:email 只是 String —— 任何垃圾都能通过
fn send_email(to: String, body: String) {
// `to` 实际上是有效的 email 吗?我们不知道。
// 有人可以传递 "not-an-email",我们只在 SMTP 服务器发现。
}
}
解决方案:使用 TryFrom 解析为已验证的新类型
use std::convert::TryFrom;
use std::fmt;
/// 已验证的 TCP 端口号(1–65535)。
/// 如果你有 `Port`,它保证有效。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Port(u16);
impl TryFrom<u16> for Port {
type Error = PortError;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value == 0 {
Err(PortError::Zero)
} else {
Ok(Port(value))
}
}
}
impl Port {
pub fn get(&self) -> u16 { self.0 }
}
#[derive(Debug)]
pub enum PortError {
Zero,
InvalidFormat,
}
impl fmt::Display for PortError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PortError::Zero => write!(f, "port must be non-zero"),
PortError::InvalidFormat => write!(f, "invalid port format"),
}
}
}
impl std::error::Error for PortError {}
// 现在类型系统强制执行有效性:
fn start_server(port: Port) {
// 不需要验证 —— Port 只能通过 TryFrom 构造,
// 它已经验证它是有效的。
println!("Listening on port {}", port.get());
}
// 用法:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let port = Port::try_from(8080)?; // ✅ 在边界处验证一次
start_server(port); // 下游无需重新验证
let bad = Port::try_from(0); // ❌ Err(PortError::Zero)
Ok(())
}
真实示例:已验证的 IPMI 地址
#![allow(unused)]
fn main() {
/// 已验证的 IPMI 从地址(0x20–0xFE,仅偶数)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IpmiAddr(u8);
#[derive(Debug)]
pub enum IpmiAddrError {
Odd(u8),
OutOfRange(u8),
}
impl fmt::Display for IpmiAddrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IpmiAddrError::Odd(v) => write!(f, "IPMI address 0x{v:02X} must be even"),
IpmiAddrError::OutOfRange(v) => {
write!(f, "IPMI address 0x{v:02X} out of range (0x20..=0xFE)")
}
}
}
}
impl TryFrom<u8> for IpmiAddr {
type Error = IpmiAddrError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value % 2 != 0 {
Err(IpmiAddrError::Odd(value))
} else if value < 0x20 || value > 0xFE {
Err(IpmiAddrError::OutOfRange(value))
} else {
Ok(IpmiAddr(value))
}
}
}
impl IpmiAddr {
pub fn get(&self) -> u8 { self.0 }
}
// 下游代码从不需要重新检查:
fn send_ipmi_command(addr: IpmiAddr, cmd: u8, data: &[u8]) -> Result<Vec<u8>, IpmiError> {
// addr.get() 保证是有效的、偶数的 IPMI 地址
raw_ipmi_send(addr.get(), cmd, data)
}
}
使用 FromStr 解析字符串
对于通常从文本解析的类型(CLI 参数、配置文件),实现 FromStr:
#![allow(unused)]
fn main() {
use std::str::FromStr;
impl FromStr for Port {
type Err = PortError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let n: u16 = s.parse().map_err(|_| PortError::InvalidFormat)?;
Port::try_from(n)
}
}
// 现在可以使用 .parse():
let port: Port = "8080".parse()?; // 一步验证
// 与 clap CLI 解析一起使用:
// #[derive(Parser)]
// struct Args {
// #[arg(short, long)]
// port: Port, // clap 自动调用 FromStr
// }
}
复杂验证的 TryFrom 链
#![allow(unused)]
fn main() {
/// 已验证的配置,只有当所有字段有效时才能存在。
pub struct ValidConfig {
pub host: Hostname,
pub port: Port,
pub timeout_ms: Timeout,
}
impl TryFrom<RawConfig> for ValidConfig {
type Error = ConfigError;
fn try_from(raw: RawConfig) -> Result<Self, Self::Error> {
Ok(ValidConfig {
host: Hostname::try_from(raw.host)
.map_err(ConfigError::InvalidHost)?,
port: Port::try_from(raw.port)
.map_err(ConfigError::InvalidPort)?,
timeout_ms: Timeout::try_from(raw.timeout_ms)
.map_err(ConfigError::InvalidTimeout)?,
})
}
}
// 在边界处解析一次,在任何地方使用已验证的类型:
fn load_config(path: &str) -> Result<ValidConfig, ConfigError> {
let raw: RawConfig = serde_json::from_str(&std::fs::read_to_string(path)?)?;
ValidConfig::try_from(raw) // 所有验证发生在这里
}
}
总结:验证 vs 解析
| 方法 | 数据检查? | 编译器强制执行有效性? | 需要重新验证? |
|---|---|---|---|
| 运行时检查(if/assert) | ✅ | ❌ | 每个函数边界 |
已验证新类型 + TryFrom | ✅ | ✅ | 从不 —— 类型是证明 |
规则:在边界处解析,在内部任何地方使用已验证类型。
原始字符串、整数和字节切片进入你的系统,通过 TryFrom/FromStr 解析为已验证类型,从那时起类型系统保证它们是有效的。
特性标志和条件编译
# Cargo.toml
[features]
default = ["json"] # 默认启用
json = ["dep:serde_json"] # 启用 JSON 支持
xml = ["dep:quick-xml"] # 启用 XML 支持
full = ["json", "xml"] # 元特性:启用所有
[dependencies]
serde = "1"
serde_json = { version = "1", optional = true }
quick-xml = { version = "0.31", optional = true }
#![allow(unused)]
fn main() {
// 基于特性的条件编译:
#[cfg(feature = "json")]
pub fn to_json<T: serde::Serialize>(value: &T) -> String {
serde_json::to_string(value).unwrap()
}
#[cfg(feature = "xml")]
pub fn to_xml<T: serde::Serialize>(value: &T) -> String {
quick_xml::se::to_string(value).unwrap()
}
// 如果未启用必需特性则编译错误:
#[cfg(not(any(feature = "json", feature = "xml")))]
compile_error!("必须启用至少一个格式特性(json、xml)");
}
最佳实践:
- 保持
default特性最小 —— 用户可以选择加入 - 使用
dep:语法(Rust 1.60+)用于可选依赖以避免创建隐式特性 - 在 README 和 crate 级文档中记录特性
工作区组织
对于大型项目,使用 Cargo 工作区共享依赖和构建产物:
# Root Cargo.toml
[workspace]
members = [
"core", # 共享类型和 trait
"parser", # 解析库
"server", # 二进制 —— 主应用程序
"client", # 客户端库
"cli", # CLI 二进制
]
# 共享依赖版本:
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
# 在每个成员的 Cargo.toml 中:
# [dependencies]
# serde = { workspace = true }
好处:
- 单个
Cargo.lock—— 所有 crate 使用相同的依赖版本 cargo test --workspace运行所有测试- 共享构建缓存 —— 编译一个 crate 有利于所有
- 组件之间清晰的依赖边界
.cargo/config.toml:项目级配置
.cargo/config.toml 文件(在工作区根目录或 $HOME/.cargo/ 中)
自定义 Cargo 行为而无需修改 Cargo.toml:
# .cargo/config.toml
# 这个工作区的默认 target
[build]
target = "x86_64-unknown-linux-gnu"
# 自定义运行器 —— 例如通过 QEMU 运行交叉编译的二进制文件
[target.aarch64-unknown-linux-gnu]
runner = "qemu-aarch64-static"
linker = "aarch64-linux-gnu-gcc"
# Cargo 别名 —— 自定义快捷命令
[alias]
xt = "test --workspace --release" # cargo xt = release 模式下运行所有测试
ci = "clippy --workspace -- -D warnings" # cargo ci = lint,警告视为错误
cov = "llvm-cov --workspace" # cargo cov = 覆盖率(需要 cargo-llvm-cov)
# 构建脚本的环境变量
[env]
IPMI_LIB_PATH = "/usr/lib/bmc"
# 使用自定义注册表(用于内部包)
# [registries.internal]
# index = "https://gitlab.internal/crates/index"
常见配置模式:
| 设置 | 目的 | 示例 |
|---|---|---|
[build] target | 默认编译 target | x86_64-unknown-linux-musl 用于静态构建 |
[target.X] runner | 如何运行二进制文件 | "qemu-aarch64-static" 用于交叉编译 |
[target.X] linker | 使用哪个链接器 | "aarch64-linux-gnu-gcc" |
[alias] | 自定义 cargo 子命令 | xt = "test --workspace" |
[env] | 构建时环境变量 | 库路径、特性切换 |
[net] offline | 防止网络访问 | true 用于隔离构建 |
编译时环境变量:env!() 和 option_env!()
Rust 可以在编译时将环境变量嵌入二进制文件 —— 对版本字符串、构建元数据和配置很有用:
#![allow(unused)]
fn main() {
// env!() —— 如果变量缺失则在编译时 panic
const VERSION: &str = env!("CARGO_PKG_VERSION"); // Cargo.toml 中的 "0.1.0"
const PKG_NAME: &str = env!("CARGO_PKG_NAME"); // Cargo.toml 中的 crate 名称
// option_env!() —— 返回 Option<&str>,如果缺失则不 panic
const BUILD_SHA: Option<&str> = option_env!("GIT_SHA");
const BUILD_TIME: Option<&str> = option_env!("BUILD_TIMESTAMP");
fn print_version() {
println!("{PKG_NAME} v{VERSION}");
if let Some(sha) = BUILD_SHA {
println!(" commit: {sha}");
}
if let Some(time) = BUILD_TIME {
println!(" built: {time}");
}
}
}
Cargo 自动设置许多有用的环境变量:
| 变量 | 值 | 使用场景 |
|---|---|---|
CARGO_PKG_VERSION | "1.2.3" | 版本报告 |
CARGO_PKG_NAME | "diag_tool" | 二进制标识 |
CARGO_PKG_AUTHORS | 来自 Cargo.toml | 关于/帮助文本 |
CARGO_MANIFEST_DIR | Cargo.toml 的绝对路径 | 定位测试数据文件 |
OUT_DIR | 构建输出目录 | build.rs 代码生成目标 |
TARGET | Target triple | build.rs 中特定于平台的逻辑 |
你可以从 build.rs 设置自定义环境变量:
// build.rs
fn main() {
println!("cargo::rustc-env=GIT_SHA={}", git_sha());
println!("cargo::rustc-env=BUILD_TIMESTAMP={}", timestamp());
}
cfg_attr:条件属性
cfg_attr 仅当条件为真时应用属性。这比 #[cfg()] 更有针对性,后者包含/排除整个项:
#![allow(unused)]
fn main() {
// 仅当启用 "serde" 特性时 derive Serialize:
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct DiagResult {
pub fc: u32,
pub passed: bool,
pub message: String,
}
// 无 "serde" 特性:根本不需要 serde 依赖
// 有 "serde" 特性:DiagResult 可序列化
// 测试的条件属性:
#[cfg_attr(test, derive(PartialEq))] // 仅在测试构建中 derive PartialEq
pub struct LargeStruct { /* ... */ }
// 平台特定函数属性:
#[cfg_attr(target_os = "linux", link_name = "ioctl")]
#[cfg_attr(target_os = "freebsd", link_name = "__ioctl")]
extern "C" fn platform_ioctl(fd: i32, request: u64) -> i32;
}
| 模式 | 做什么 |
|---|---|
#[cfg(feature = "x")] | 包含/排除整个项 |
#[cfg_attr(feature = "x", derive(Foo))] | 仅当启用特性 “x” 时添加 derive(Foo) |
#[cfg_attr(test, allow(unused))] | 仅在测试构建中抑制警告 |
#[cfg_attr(doc, doc = "...")] | 仅在 cargo doc 中可见的文档 |
cargo deny 和 cargo audit:供应链安全
# 安装安全审计工具
cargo install cargo-deny
cargo install cargo-audit
# 检查依赖中的已知漏洞
cargo audit
# 综合检查:许可证、禁止、咨询、来源
cargo deny check
使用工作区根目录的 deny.toml 配置 cargo deny:
# deny.toml
[advisories]
vulnerability = "deny" # 拒绝已知漏洞
unmaintained = "warn" # 警告不再维护的 crate
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause"]
deny = ["GPL-3.0"] # 拒绝 copyleft 许可证
[bans]
multiple-versions = "warn" # 如果同一 crate 有多个版本则警告
deny = [
{ name = "openssl" }, # 强制使用 rustls
]
[sources]
allow-git = [] # 生产环境中无 git 依赖
| 工具 | 目的 | 何时运行 |
|---|---|---|
cargo audit | 检查依赖中的已知 CVE | CI 流水线、发布前 |
cargo deny check | 许可证、禁止、咨询、来源 | CI 流水线 |
cargo deny check licenses | 仅许可证合规 | 开源前 |
cargo deny check bans | 防止特定 crate | 强制执行架构决策 |
文档测试:文档中的测试
Rust 文档注释(///)可以包含作为测试编译和运行的代码块:
#![allow(unused)]
fn main() {
/// 从字符串解析诊断错误码。
///
/// # Examples
///
/// ```
/// use my_crate::parse_fc;
///
/// let fc = parse_fc("FC:12345").unwrap();
/// assert_eq!(fc, 12345);
/// ```
///
/// 无效输入返回错误:
///
/// ```
/// use my_crate::parse_fc;
///
/// assert!(parse_fc("not-a-fc").is_err());
/// ```
pub fn parse_fc(input: &str) -> Result<u32, ParseError> {
input.strip_prefix("FC:")
.ok_or(ParseError::MissingPrefix)?
.parse()
.map_err(ParseError::InvalidNumber)
}
}
cargo test --doc # 仅运行文档测试
cargo test # 运行单元 + 集成 + 文档测试
模块级文档在文件顶部使用 //!:
#![allow(unused)]
fn main() {
//! # 诊断框架
//!
//! 这个 crate 提供核心诊断执行引擎。
//! 它支持运行诊断测试、收集结果、
//! 并通过 IPMI 报告给 BMC。
//!
//! ## 快速入门
//!
//! ```no_run
//! use diag_framework::Framework;
//!
//! let mut fw = Framework::new("config.json")?;
//! fw.run_all_tests()?;
//! ```
}
使用 Criterion 进行基准测试
完整覆盖:参见 使用 criterion 进行基准测试 第 13 章(测试和基准测试模式)中的完整
criterion设置、API 示例和与cargo bench的比较表。 下面是特定于架构的用法的快速参考。
在对 crate 的公共 API 进行基准测试时,将基准测试放在 benches/ 中,
并专注于热点路径 —— 通常是解析器、序列化器或验证边界:
cargo bench # 运行所有基准测试
cargo bench -- parse_config # 运行特定基准测试
# 结果在 target/criterion/ 中,带有 HTML 报告
关键要点 —— 架构和 API 设计
- 接受最通用的类型(
impl Into、impl AsRef、Cow);返回最具体的- 解析不要验证:使用
TryFrom创建通过构造有效的类型- 公共枚举上的
#[non_exhaustive]防止添加变体时的破坏性变更#[must_use]捕获重要值的静默丢弃
另见: 第 9 章 — 错误处理 了解公共 API 中的错误类型设计。第 13 章 — 测试 了解测试 crate 的公共 API。
练习:Crate API 重构 ★★(约 30 分钟)
将以下“字符串类型“的 API 重构为使用 TryFrom、新类型和 builder 模式的 API:
// 之前:容易误用
fn create_server(host: &str, port: &str, max_conn: &str) -> Server { ... }
设计一个 ServerConfig,带有已验证类型 Host、Port(1–65535)和 MaxConnections(1–10000),在解析时拒绝无效值。
🔑 解答
#[derive(Debug, Clone)]
struct Host(String);
impl TryFrom<&str> for Host {
type Error = String;
fn try_from(s: &str) -> Result<Self, String> {
if s.is_empty() { return Err("host cannot be empty".into()); }
if s.contains(' ') { return Err("host cannot contain spaces".into()); }
Ok(Host(s.to_string()))
}
}
#[derive(Debug, Clone, Copy)]
struct Port(u16);
impl TryFrom<u16> for Port {
type Error = String;
fn try_from(p: u16) -> Result<Self, String> {
if p == 0 { return Err("port must be >= 1".into()); }
Ok(Port(p))
}
}
#[derive(Debug, Clone, Copy)]
struct MaxConnections(u32);
impl TryFrom<u32> for MaxConnections {
type Error = String;
fn try_from(n: u32) -> Result<Self, String> {
if n == 0 || n > 10_000 {
return Err(format!("max_connections must be 1–10000, got {n}"));
}
Ok(MaxConnections(n))
}
}
#[derive(Debug)]
struct ServerConfig {
host: Host,
port: Port,
max_connections: MaxConnections,
}
impl ServerConfig {
fn new(host: Host, port: Port, max_connections: MaxConnections) -> Self {
ServerConfig { host, port, max_connections }
}
}
fn main() {
let config = ServerConfig::new(
Host::try_from("localhost").unwrap(),
Port::try_from(8080).unwrap(),
MaxConnections::try_from(100).unwrap(),
);
println!("{config:?}");
// 在解析时捕获无效值:
assert!(Host::try_from("").is_err());
assert!(Port::try_from(0).is_err());
assert!(MaxConnections::try_from(99999).is_err());
}
16. Async/Await Essentials 🔴
你将学到:
- Rust 的
Futuretrait 与 Go 的 goroutine 和 Python 的 asyncio 有何不同- Tokio 快速入门:生成任务、
join!和运行时配置- 常见的 async 陷阱以及如何修复
- 何时使用
spawn_blocking卸载阻塞工作
Futures、Runtimes 和 async fn
Rust 的 async 模型与 Go 的 goroutine 或 Python 的 asyncio 根本不同。
理解三个概念就足以开始:
Future是惰性状态机 —— 调用async fn不会执行任何操作; 它返回一个必须被轮询(polled)的Future。- 你需要一个运行时 来轮询 futures ——
tokio、async-std或smol。 标准库定义了Future但不提供运行时。 async fn是语法糖 —— 编译器将其转换为实现Future的状态机。
#![allow(unused)]
fn main() {
// Future 只是一个 trait:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
// async fn 脱糖为:
// fn fetch_data(url: &str) -> impl Future<Output = Result<Vec<u8>, Error>>
async fn fetch_data(url: &str) -> Result<Vec<u8>, reqwest::Error> {
let response = reqwest::get(url).await?; // .await 挂起直到就绪
let bytes = response.bytes().await?;
Ok(bytes.to_vec())
}
}
Tokio 快速入门
# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};
use tokio::task;
#[tokio::main]
async fn main() {
// 生成并发任务(类似轻量级线程):
let handle_a = task::spawn(async {
sleep(Duration::from_millis(100)).await;
"task A done"
});
let handle_b = task::spawn(async {
sleep(Duration::from_millis(50)).await;
"task B done"
});
// .await 两者 —— 它们并发运行,而非顺序:
let (a, b) = tokio::join!(handle_a, handle_b);
println!("{}, {}", a.unwrap(), b.unwrap());
}
Async 常见陷阱
| 陷阱 | 发生原因 | 修复 |
|---|---|---|
| 阻塞 async | std::thread::sleep 或 CPU 工作阻塞执行器 | 使用 tokio::task::spawn_blocking 或 rayon |
Send 边界错误 | Future 在 .await 期间持有 !Send 类型(如 Rc、MutexGuard) | 重构代码在 .await 之前 drop 非 Send 值 |
| Future 未被轮询 | 调用 async fn 但不 .await 或生成 —— 什么都不发生 | 总是 .await 或 tokio::spawn 返回的 future |
在 .await 间持有 MutexGuard | std::sync::MutexGuard 是 !Send;async 任务可能在不同线程恢复 | 使用 tokio::sync::Mutex 或在 .await 前 drop guard |
| 意外顺序执行 | let a = foo().await; let b = bar().await; 顺序运行 | 使用 tokio::join! 或 tokio::spawn 实现并发 |
#![allow(unused)]
fn main() {
// ❌ 阻塞 async 执行器:
async fn bad() {
std::thread::sleep(std::time::Duration::from_secs(5)); // 阻塞整个线程!
}
// ✅ 卸载阻塞工作:
async fn good() {
tokio::task::spawn_blocking(|| {
std::thread::sleep(std::time::Duration::from_secs(5)); // 在阻塞池上运行
}).await.unwrap();
}
}
全面的 async 覆盖:关于
Stream、select!、取消安全、 结构化并发和tower中间件,请参阅我们专门的 Async Rust 培训指南。本节仅涵盖读写基本 async 代码的足够知识。
生成和结构化并发
Tokio 的 spawn 创建一个新的异步任务 —— 类似于 thread::spawn 但
更轻量:
use tokio::task;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// 生成三个并发任务
let h1 = task::spawn(async {
sleep(Duration::from_millis(200)).await;
"fetched user profile"
});
let h2 = task::spawn(async {
sleep(Duration::from_millis(100)).await;
"fetched order history"
});
let h3 = task::spawn(async {
sleep(Duration::from_millis(150)).await;
"fetched recommendations"
});
// 等待所有三个并发(不是顺序!)
let (r1, r2, r3) = tokio::join!(h1, h2, h3);
println!("{}", r1.unwrap());
println!("{}", r2.unwrap());
println!("{}", r3.unwrap());
}
join! vs try_join! vs select!:
| 宏 | 行为 | 何时使用 |
|---|---|---|
join! | 等待所有 futures | 所有任务必须完成 |
try_join! | 等待所有,遇到第一个 Err 时短路 | 任务返回 Result |
select! | 当第一个 future 完成时返回 | 超时、取消 |
use tokio::time::{timeout, Duration};
async fn fetch_with_timeout() -> Result<String, Box<dyn std::error::Error>> {
let result = timeout(Duration::from_secs(5), async {
// 模拟慢网络调用
tokio::time::sleep(Duration::from_millis(100)).await;
Ok::<_, Box<dyn std::error::Error>>("data".to_string())
}).await??; // 第一个 ? 解包 Elapsed,第二个 ? 解包内部 Result
Ok(result)
}
Send 边界以及为什么 Futures 必须是 Send
当你 tokio::spawn 一个 future 时,它可能在不同的 OS 线程上恢复。
这意味着 future 必须是 Send。常见陷阱:
use std::rc::Rc;
async fn not_send() {
let rc = Rc::new(42); // Rc 是 !Send
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("{}", rc); // rc 在 .await 间持有 —— future 是 !Send
}
// 修复 1:在 .await 前 drop
async fn fixed_drop() {
let data = {
let rc = Rc::new(42);
*rc // 复制值出来
}; // rc 在这里 drop
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("{}", data); // 只是 i32,它是 Send
}
// 修复 2:用 Arc 替代 Rc
async fn fixed_arc() {
let arc = std::sync::Arc::new(42); // Arc 是 Send
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("{}", arc); // ✅ Future 是 Send
}
全面的 async 覆盖:关于
Stream、select!、取消安全、 结构化并发和tower中间件,请参阅我们专门的 Async Rust 培训指南。本节仅涵盖读写基本 async 代码的足够知识。
另见: 第 5 章 — 通道 了解同步通道。第 6 章 — 并发 了解 OS 线程 vs async 任务。
关键要点 —— Async
async fn返回惰性Future—— 直到你.await或生成它之前都不会运行- 对 CPU 密集型或阻塞工作使用
tokio::task::spawn_blocking- 不要在
.await间持有std::sync::MutexGuard—— 改用tokio::sync::Mutex- Futures 生成时必须是
Send—— 在.await点之前 drop!Send类型
练习:带超时的并发抓取器 ★★(约 25 分钟)
编写一个 async 函数 fetch_all,生成三个 tokio::spawn 任务,每个
模拟使用 tokio::time::sleep 的网络调用。使用
tokio::try_join! 连接所有三个,并用 tokio::time::timeout(Duration::from_secs(5), ...) 包装。
返回 Result<Vec<String>, ...> 或如果任何任务失败或截止日期过期则返回错误。
🔑 解答
use tokio::time::{sleep, timeout, Duration};
async fn fake_fetch(name: &'static str, delay_ms: u64) -> Result<String, String> {
sleep(Duration::from_millis(delay_ms)).await;
Ok(format!("{name}: OK"))
}
async fn fetch_all() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let deadline = Duration::from_secs(5);
let (a, b, c) = timeout(deadline, async {
let h1 = tokio::spawn(fake_fetch("svc-a", 100));
let h2 = tokio::spawn(fake_fetch("svc-b", 200));
let h3 = tokio::spawn(fake_fetch("svc-c", 150));
tokio::try_join!(h1, h2, h3)
})
.await??;
Ok(vec![a?, b?, c?])
}
#[tokio::main]
async fn main() {
let results = fetch_all().await.unwrap();
for r in &results {
println!("{r}");
}
}
17. 练习
练习
练习 1:类型安全状态机 ★★(约 30 分钟)
使用 type-state 模式构建一个交通灯状态机。灯必须按 Red → Green → Yellow → Red 转换,其他顺序都不应该可能。
🔑 解答
use std::marker::PhantomData;
struct Red;
struct Green;
struct Yellow;
struct TrafficLight<State> {
_state: PhantomData<State>,
}
impl TrafficLight<Red> {
fn new() -> Self {
println!("🔴 Red — STOP");
TrafficLight { _state: PhantomData }
}
fn go(self) -> TrafficLight<Green> {
println!("🟢 Green — GO");
TrafficLight { _state: PhantomData }
}
}
impl TrafficLight<Green> {
fn caution(self) -> TrafficLight<Yellow> {
println!("🟡 Yellow — CAUTION");
TrafficLight { _state: PhantomData }
}
}
impl TrafficLight<Yellow> {
fn stop(self) -> TrafficLight<Red> {
println!("🔴 Red — STOP");
TrafficLight { _state: PhantomData }
}
}
fn main() {
let light = TrafficLight::new(); // Red
let light = light.go(); // Green
let light = light.caution(); // Yellow
let light = light.stop(); // Red
// light.caution(); // ❌ 编译错误:Red 上没有方法 `caution`
// TrafficLight::new().stop(); // ❌ 编译错误:Red 上没有方法 `stop`
}
关键要点:无效的转换是编译错误,而非运行时 panic。
练习 2:带 PhantomData 的测量单位 ★★(约 30 分钟)
扩展第 4 章的测量单位模式以支持:
Meters、Seconds、Kilograms- 相同单位的加法
- 乘法:
Meters * Meters = SquareMeters - 除法:
Meters / Seconds = MetersPerSecond
🔑 解答
use std::marker::PhantomData;
use std::ops::{Add, Mul, Div};
#[derive(Clone, Copy)]
struct Meters;
#[derive(Clone, Copy)]
struct Seconds;
#[derive(Clone, Copy)]
struct Kilograms;
#[derive(Clone, Copy)]
struct SquareMeters;
#[derive(Clone, Copy)]
struct MetersPerSecond;
#[derive(Debug, Clone, Copy)]
struct Qty<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Qty<U> {
fn new(v: f64) -> Self { Qty { value: v, _unit: PhantomData } }
}
impl<U> Add for Qty<U> {
type Output = Qty<U>;
fn add(self, rhs: Self) -> Self::Output { Qty::new(self.value + rhs.value) }
}
impl Mul<Qty<Meters>> for Qty<Meters> {
type Output = Qty<SquareMeters>;
fn mul(self, rhs: Qty<Meters>) -> Qty<SquareMeters> {
Qty::new(self.value * rhs.value)
}
}
impl Div<Qty<Seconds>> for Qty<Meters> {
type Output = Qty<MetersPerSecond>;
fn div(self, rhs: Qty<Seconds>) -> Qty<MetersPerSecond> {
Qty::new(self.value / rhs.value)
}
}
fn main() {
let width = Qty::<Meters>::new(5.0);
let height = Qty::<Meters>::new(3.0);
let area = width * height; // Qty<SquareMeters>
println!("Area: {:.1} m²", area.value);
let dist = Qty::<Meters>::new(100.0);
let time = Qty::<Seconds>::new(9.58);
let speed = dist / time;
println!("Speed: {:.2} m/s", speed.value);
let sum = width + height; // 相同单位 ✅
println!("Sum: {:.1} m", sum.value);
// let bad = width + time; // ❌ 编译错误:不能相加 Meters + Seconds
}
练习 3:基于通道的 worker 池 ★★★(约 45 分钟)
使用通道构建一个 worker 池,其中:
- 调度器通过通道发送
Job结构体 - N 个 worker 消费作业并发送结果回来
- 使用
crossbeam-channel(如果不可用则使用std::sync::mpsc)
🔑 解答
use std::sync::mpsc;
use std::thread;
struct Job {
id: u64,
data: String,
}
struct JobResult {
job_id: u64,
output: String,
worker_id: usize,
}
fn worker_pool(jobs: Vec<Job>, num_workers: usize) -> Vec<JobResult> {
let (job_tx, job_rx) = mpsc::channel::<Job>();
let (result_tx, result_rx) = mpsc::channel::<JobResult>();
// 在 Arc<Mutex> 中包装接收器以便在 worker 之间共享
let job_rx = std::sync::Arc::new(std::sync::Mutex::new(job_rx));
// 生成 worker
let mut handles = Vec::new();
for worker_id in 0..num_workers {
let job_rx = job_rx.clone();
let result_tx = result_tx.clone();
handles.push(thread::spawn(move || {
loop {
// 锁定、接收、解锁 —— 短临界区
let job = {
let rx = job_rx.lock().unwrap();
rx.recv() // 阻塞直到有作业或通道关闭
};
match job {
Ok(job) => {
let output = format!("processed '{}' by worker {worker_id}", job.data);
result_tx.send(JobResult {
job_id: job.id,
output,
worker_id,
}).unwrap();
}
Err(_) => break, // 通道关闭 —— 退出
}
}
}));
}
drop(result_tx); // 丢弃我们的副本以便结果通道在 worker 完成时关闭
// 分发作业
let num_jobs = jobs.len();
for job in jobs {
job_tx.send(job).unwrap();
}
drop(job_tx); // 关闭作业通道 —— worker 在 draining 后退出
// 收集结果
let mut results = Vec::new();
for result in result_rx {
results.push(result);
}
assert_eq!(results.len(), num_jobs);
for h in handles { h.join().unwrap(); }
results
}
fn main() {
let jobs: Vec<Job> = (0..20).map(|i| Job {
id: i,
data: format!("task-{i}"),
}).collect();
let results = worker_pool(jobs, 4);
for r in &results {
println!("[worker {}] job {}: {}", r.worker_id, r.job_id, r.output);
}
}
练习 4:高阶组合器管道 ★★(约 25 分钟)
创建一个 Pipeline 结构体来链接转换。它应该支持 .pipe(f) 添加转换和 .execute(input) 运行完整链。
🔑 解答
struct Pipeline<T> {
transforms: Vec<Box<dyn Fn(T) -> T>>,
}
impl<T: 'static> Pipeline<T> {
fn new() -> Self {
Pipeline { transforms: Vec::new() }
}
fn pipe(mut self, f: impl Fn(T) -> T + 'static) -> Self {
self.transforms.push(Box::new(f));
self
}
fn execute(self, input: T) -> T {
self.transforms.into_iter().fold(input, |val, f| f(val))
}
}
fn main() {
let result = Pipeline::new()
.pipe(|s: String| s.trim().to_string())
.pipe(|s| s.to_uppercase())
.pipe(|s| format!(">>> {s} <<<"))
.execute(" hello world ".to_string());
println!("{result}"); // >>> HELLO WORLD <<<
// 数字管道:
let result = Pipeline::new()
.pipe(|x: i32| x * 2)
.pipe(|x| x + 10)
.pipe(|x| x * x)
.execute(5);
println!("{result}"); // (5*2 + 10)^2 = 400
}
Bonus:在阶段之间改变类型的通用管道将使用不同的设计 —— 每个 .pipe() 返回一个具有不同输出类型的 Pipeline(这需要更高级的泛型处理)。
练习 5:带 thiserror 的错误层次 ★★(约 30 分钟)
为文件处理应用程序设计错误类型层次结构,该程序可能在 I/O、解析(JSON 和 CSV)和验证期间失败。使用 thiserror 并演示 ? 传播。
🔑 解答
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("CSV error at line {line}: {message}")]
Csv { line: usize, message: String },
#[error("validation error: {field} — {reason}")]
Validation { field: String, reason: String },
}
fn read_file(path: &str) -> Result<String, AppError> {
Ok(std::fs::read_to_string(path)?) // io::Error → AppError::Io via #[from]
}
fn parse_json(content: &str) -> Result<serde_json::Value, AppError> {
Ok(serde_json::from_str(content)?) // serde_json::Error → AppError::Json
}
fn validate_name(value: &serde_json::Value) -> Result<String, AppError> {
let name = value.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| AppError::Validation {
field: "name".into(),
reason: "must be a non-null string".into(),
})?;
if name.is_empty() {
return Err(AppError::Validation {
field: "name".into(),
reason: "must not be empty".into(),
});
}
Ok(name.to_string())
}
fn process_file(path: &str) -> Result<String, AppError> {
let content = read_file(path)?;
let json = parse_json(&content)?;
let name = validate_name(&json)?;
Ok(name)
}
fn main() {
match process_file("config.json") {
Ok(name) => println!("Name: {name}"),
Err(e) => eprintln!("Error: {e}"),
}
}
练习 6:带关联类型的泛型 trait ★★★(约 40 分钟)
设计一个带有 Error 和 Id 关联类型的 Repository<T> trait。为内存存储实现它并演示编译时类型安全。
🔑 解答
use std::collections::HashMap;
trait Repository {
type Item;
type Id;
type Error;
fn get(&self, id: &Self::Id) -> Result<Option<&Self::Item>, Self::Error>;
fn insert(&mut self, item: Self::Item) -> Result<Self::Id, Self::Error>;
fn delete(&mut self, id: &Self::Id) -> Result<bool, Self::Error>;
}
#[derive(Debug, Clone)]
struct User {
name: String,
email: String,
}
struct InMemoryUserRepo {
data: HashMap<u64, User>,
next_id: u64,
}
impl InMemoryUserRepo {
fn new() -> Self {
InMemoryUserRepo { data: HashMap::new(), next_id: 1 }
}
}
// 错误类型是 Infallible —— 内存操作从失败
impl Repository for InMemoryUserRepo {
type Item = User;
type Id = u64;
type Error = std::convert::Infallible;
fn get(&self, id: &u64) -> Result<Option<&User>, Self::Error> {
Ok(self.data.get(id))
}
fn insert(&mut self, item: User) -> Result<u64, Self::Error> {
let id = self.next_id;
self.next_id += 1;
self.data.insert(id, item);
Ok(id)
}
fn delete(&mut self, id: &u64) -> Result<bool, Self::Error> {
Ok(self.data.remove(id).is_some())
}
}
// 泛型函数适用于 ANY repository:
fn create_and_fetch<R: Repository>(repo: &mut R, item: R::Item) -> Result<(), R::Error>
where
R::Item: std::fmt::Debug,
R::Id: std::fmt::Debug,
{
let id = repo.insert(item)?;
println!("Inserted with id: {id:?}");
let retrieved = repo.get(&id)?;
println!("Retrieved: {retrieved:?}");
Ok(())
}
fn main() {
let mut repo = InMemoryUserRepo::new();
create_and_fetch(&mut repo, User {
name: "Alice".into(),
email: "alice@example.com".into(),
}).unwrap();
}
练习 7:Unsafe 之上的安全包装器(第 11 章)★★★(约 45 分钟)
编写一个 FixedVec<T, const N: usize> —— 固定容量、栈分配的向量。
要求:
push(&mut self, value: T) -> Result<(), T>满时返回Err(value)pop(&mut self) -> Option<T>返回并移除最后一个元素as_slice(&self) -> &[T]借用已初始化的元素- 所有公共方法必须是安全的;所有 unsafe 必须用
SAFETY:注释封装 Drop必须清理已初始化的元素
Hint:使用 MaybeUninit<T> 和 [const { MaybeUninit::uninit() }; N]。
🔑 解答
use std::mem::MaybeUninit;
pub struct FixedVec<T, const N: usize> {
data: [MaybeUninit<T>; N],
len: usize,
}
impl<T, const N: usize> FixedVec<T, N> {
pub fn new() -> Self {
FixedVec {
data: [const { MaybeUninit::uninit() }; N],
len: 0,
}
}
pub fn push(&mut self, value: T) -> Result<(), T> {
if self.len >= N { return Err(value); }
// SAFETY: len < N,所以 data[len] 在边界内。
self.data[self.len] = MaybeUninit::new(value);
self.len += 1;
Ok(())
}
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 { return None; }
self.len -= 1;
// SAFETY: data[len] 已初始化(递减前 len > 0)。
Some(unsafe { self.data[self.len].assume_init_read() })
}
pub fn as_slice(&self) -> &[T] {
// SAFETY: data[0..len] 都已初始化,且 MaybeUninit<T>
// 与 T 有相同的布局。
unsafe { std::slice::from_raw_parts(self.data.as_ptr() as *const T, self.len) }
}
pub fn len(&self) -> usize { self.len }
pub fn is_empty(&self) -> bool { self.len == 0 }
}
impl<T, const N: usize> Drop for FixedVec<T, N> {
fn drop(&mut self) {
// SAFETY: data[0..len] 都已初始化 —— drop 每一个。
for i in 0..self.len {
unsafe { self.data[i].assume_init_drop(); }
}
}
}
fn main() {
let mut v = FixedVec::<String, 4>::new();
v.push("hello".into()).unwrap();
v.push("world".into()).unwrap();
assert_eq!(v.as_slice(), &["hello", "world"]);
assert_eq!(v.pop(), Some("world".into()));
assert_eq!(v.len(), 1);
// Drop 清理剩余的 "hello"
}
练习 8:声明式宏 —— map!(第 12 章)★(约 15 分钟)
编写一个 map! 宏,从键值对创建 HashMap,类似于 vec![]:
#![allow(unused)]
fn main() {
let m = map! {
"host" => "localhost",
"port" => "8080",
};
assert_eq!(m.get("host"), Some(&"localhost"));
assert_eq!(m.len(), 2);
}
要求:
- 支持尾随逗号
- 支持空调用
map!{} - 适用于任何为
Into<K>和Into<V>实现类型的类型以获得最大灵活性
🔑 解答
macro_rules! map {
// 空情况
() => {
std::collections::HashMap::new()
};
// 一个或多个 key => value 对(尾随逗号可选)
( $( $key:expr => $val:expr ),+ $(,)? ) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($key, $val); )+
m
}};
}
fn main() {
// 基本使用:
let config = map! {
"host" => "localhost",
"port" => "8080",
"timeout" => "30",
};
assert_eq!(config.len(), 3);
assert_eq!(config["host"], "localhost");
// 空 map:
let empty: std::collections::HashMap<String, String> = map!();
assert!(empty.is_empty());
// 不同类型:
let scores = map! {
1 => 100,
2 => 200,
};
assert_eq!(scores[&1], 100);
}
练习 9:自定义 serde 反序列化(第 10 章)★★★(约 45 分钟)
设计一个 Duration 包装器,使用自定义 serde 反序列化器从人类可读的字符串(如 "30s"、"5m"、"2h")反序列化。结构体还应该序列化回相同格式。
🔑 解答
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
struct HumanDuration(std::time::Duration);
impl HumanDuration {
fn from_str(s: &str) -> Result<Self, String> {
let s = s.trim();
if s.is_empty() { return Err("empty duration string".into()); }
let (num_str, suffix) = s.split_at(
s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len())
);
let value: u64 = num_str.parse()
.map_err(|_| format!("invalid number: {num_str}"))?;
let duration = match suffix {
"s" | "sec" => std::time::Duration::from_secs(value),
"m" | "min" => std::time::Duration::from_secs(value * 60),
"h" | "hr" => std::time::Duration::from_secs(value * 3600),
"ms" => std::time::Duration::from_millis(value),
other => return Err(format!("unknown suffix: {other}")),
};
Ok(HumanDuration(duration))
}
}
impl fmt::Display for HumanDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let secs = self.0.as_secs();
if secs == 0 {
write!(f, "{}ms", self.0.as_millis())
} else if secs % 3600 == 0 {
write!(f, "{}h", secs / 3600)
} else if secs % 60 == 0 {
write!(f, "{}m", secs / 60)
} else {
write!(f, "{}s", secs)
}
}
}
impl Serialize for HumanDuration {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for HumanDuration {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
HumanDuration::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Deserialize, Serialize)]
struct Config {
timeout: HumanDuration,
retry_interval: HumanDuration,
}
fn main() {
let json = r#"{ "timeout": "30s", "retry_interval": "5m" }"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.timeout.0, std::time::Duration::from_secs(30));
assert_eq!(config.retry_interval.0, std::time::Duration::from_secs(300));
// 正确往返:
let serialized = serde_json::to_string(&config).unwrap();
assert!(serialized.contains("30s"));
assert!(serialized.contains("5m"));
println!("Config: {serialized}");
}
练习 10 —— 带超时的并发抓取器 ★★(约 25 分钟)
编写一个 async 函数 fetch_all,生成三个 tokio::spawn 任务,每个模拟使用 tokio::time::sleep 的网络调用。使用 tokio::try_join! 连接所有三个,并用 tokio::time::timeout(Duration::from_secs(5), ...) 包装。返回 Result<Vec<String>, ...> 或如果任何任务失败或截止日期过期则返回错误。
学习目标:tokio::spawn、try_join!、timeout、跨任务边界的错误传播。
提示
每个生成的任务返回 Result<String, _>。try_join! 解包所有三个。
将整个 try_join! 包装在 timeout() 中 —— Elapsed 错误意味着你达到了截止日期。
解答
use tokio::time::{sleep, timeout, Duration};
async fn fake_fetch(name: &'static str, delay_ms: u64) -> Result<String, String> {
sleep(Duration::from_millis(delay_ms)).await;
Ok(format!("{name}: OK"))
}
async fn fetch_all() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let deadline = Duration::from_secs(5);
let (a, b, c) = timeout(deadline, async {
let h1 = tokio::spawn(fake_fetch("svc-a", 100));
let h2 = tokio::spawn(fake_fetch("svc-b", 200));
let h3 = tokio::spawn(fake_fetch("svc-c", 150));
tokio::try_join!(h1, h2, h3)
})
.await??; // 第一个 ? = timeout, 第二个 ? = join
Ok(vec![a?, b?, c?]) // 解包内部 Results
}
#[tokio::main]
async fn main() {
let results = fetch_all().await.unwrap();
for r in &results {
println!("{r}");
}
}
练习 11 —— Async 通道管道 ★★★(约 40 分钟)
使用 tokio::sync::mpsc 构建 producer → transformer → consumer 管道:
- Producer:发送整数 1..=20 到通道 A(容量 4)。
- Transformer:从通道 A 读取,平方每个值,发送到通道 B。
- Consumer:从通道 B 读取,收集到
Vec<u64>,返回它。
所有三个阶段作为并发 tokio::spawn 任务运行。使用有界通道演示背压。断言最终 vec 等于 [1, 4, 9, ..., 400]。
学习目标:mpsc::channel、有界背压、带 move 闭包的 tokio::spawn、通过通道关闭实现优雅关闭。
解答
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx_a, mut rx_a) = mpsc::channel::<u64>(4); // 有界 —— 背压
let (tx_b, mut rx_b) = mpsc::channel::<u64>(4);
// Producer
let producer = tokio::spawn(async move {
for i in 1..=20u64 {
tx_a.send(i).await.unwrap();
}
// tx_a 在这里 drop —— 通道 A 关闭
});
// Transformer
let transformer = tokio::spawn(async move {
while let Some(val) = rx_a.recv().await {
tx_b.send(val * val).await.unwrap();
}
// tx_b 在这里 drop —— 通道 B 关闭
});
// Consumer
let consumer = tokio::spawn(async move {
let mut results = Vec::new();
while let Some(val) = rx_b.recv().await {
results.push(val);
}
results
});
producer.await.unwrap();
transformer.await.unwrap();
let results = consumer.await.unwrap();
let expected: Vec<u64> = (1..=20).map(|x: u64| x * x).collect();
assert_eq!(results, expected);
println!("Pipeline complete: {results:?}");
}
总结与参考卡
快速参考卡
模式决策指南
需要原语的类型安全?
└── Newtype 模式(第 3 章)
需要编译时状态强制?
└── Type-state 模式(第 3 章)
需要无运行时数据的"标签"?
└── PhantomData(第 4 章)
需要打破 Rc/Arc 引用循环?
└── Weak<T> / sync::Weak<T>(第 8 章)
需要在不 busy-looping 的情况下等待条件?
└── Condvar + Mutex(第 6 章)
需要处理"N 个类型中的一个"?
├── 已知封闭集合 → Enum
├── 开放集合,热路径 → 泛型
├── 开放集合,冷路径 → dyn Trait
└── 完全未知的类型 → Any + TypeId(第 2 章)
需要跨线程共享状态?
├── 简单计数器/标志 → Atomics
├── 短临界区 → Mutex
├── 读多写少 → RwLock
├── 惰性一次性初始化 → OnceLock / LazyLock(第 6 章)
└── 复杂状态 → Actor + Channels
需要并行化计算?
├── 集合处理 → rayon::par_iter
├── 后台任务 → thread::spawn
└── 借用本地数据 → thread::scope
需要 async I/O 或并发网络?
├── 基础 → tokio + async/await(第 15 章)
└── 高级(streams、中间件)→ 参见 Async Rust 培训
需要错误处理?
├── 库 → thiserror(#[derive(Error)])
└── 应用程序 → anyhow(Result<T>)
需要防止值被 move?
└── Pin<T>(第 8 章)—— Futures、自引用类型所必需
Trait 边界速查表
| 边界 | 含义 |
|---|---|
T: Clone | 可以复制 |
T: Send | 可以移动到另一个线程 |
T: Sync | &T 可以在线程间共享 |
T: 'static | 不包含非 ’static 引用 |
T: Sized | 大小在编译时已知(默认) |
T: ?Sized | 大小可能未知([T]、dyn Trait) |
T: Unpin | pinning 后 move 安全 |
T: Default | 有默认值 |
T: Into<U> | 可以转换为 U |
T: AsRef<U> | 可以借用为 &U |
T: Deref<Target = U> | 自动解引用为 &U |
F: Fn(A) -> B | 可调用,不可变借用状态 |
F: FnMut(A) -> B | 可调用,可以突变状态 |
F: FnOnce(A) -> B | 可调用恰好一次,可以消费状态 |
生命周期省略规则
编译器在三种情况下自动插入生命周期(所以你不必):
#![allow(unused)]
fn main() {
// 规则 1:每个引用参数获得自己的生命周期
// fn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str)
// 规则 2:如果只有一个输入生命周期,它用于所有输出
// fn foo(x: &str) -> &str → fn foo<'a>(x: &'a str) -> &'a str
// 规则 3:如果一个参数是 &self 或 &mut self,使用它的生命周期
// fn foo(&self, x: &str) -> &str → fn foo<'a>(&'a self, x: &str) -> &'a str
}
当你必须编写显式生命周期时:
- 多个输入引用和一个引用输出(编译器无法猜测哪个输入)
- 持有引用的结构体字段:
struct Ref<'a> { data: &'a str } - 当你需要无借用引用的数据时的
'static边界
常见的 Derive Traits
#![allow(unused)]
fn main() {
#[derive(
Debug, // {:?} 格式化
Clone, // .clone()
Copy, // 隐式 copy(仅用于简单类型)
PartialEq, Eq, // == 比较
PartialOrd, Ord, // < > 比较 + 排序
Hash, // HashMap/HashSet 键
Default, // Type::default()
)]
struct MyType { /* ... */ }
}
模块可见性快速参考
pub → 对所有地方可见
pub(crate) → 在 crate 内可见
pub(super) → 对父模块可见
pub(in path) → 在特定路径内可见
(无) → 对当前模块 + 子模块私有
延伸阅读
| 资源 | 理由 |
|---|---|
| Rust Design Patterns | 惯用模式和反模式目录 |
| Rust API Guidelines | 精细公共 API 的官方清单 |
| Rust Atomics and Locks | Mara Bos 深入讲解并发原语 |
| The Rustonomicon | unsafe Rust 和黑暗角落的官方指南 |
| Error Handling in Rust | Andrew Gallant 的全面指南 |
| Jon Gjengset — Crust of Rust series | 深入讲解迭代器、生命周期、通道等 |
| Effective Rust | 改进 Rust 代码的 35 种具体方法 |
Rust 模式和工程实战 完
综合项目:类型安全的任务调度器
本项目将全书的模式整合到一个单一的、生产级别的系统中。你将构建一个类型安全的并发任务调度器,它使用泛型、traits、typestate、通道、错误处理和测试。
预计时间:4-6 小时 | 难度:★★★
你将练习:
- 泛型和 trait 边界(第 1-2 章)
- 任务生命周期的 typestate 模式(第 3 章)
- 零成本状态标记的 PhantomData(第 4 章)
- worker 通信的通道(第 5 章)
- 使用作用域线程的并发(第 6 章)
- 使用
thiserror的错误处理(第 9 章)- 使用属性基测试进行测试(第 13 章)
- 使用
TryFrom和验证类型的 API 设计(第 14 章)
问题
构建一个任务调度器,其中:
- 任务具有类型化的生命周期:
Pending → Running → Completed(或Failed) - Workers 从通道获取任务,执行它们,并报告结果
- 调度器 管理任务提交、worker 协调和结果收集
- 无效的状态转换是编译时错误
stateDiagram-v2
[*] --> Pending: scheduler.submit(task)
Pending --> Running: worker picks up task
Running --> Completed: task succeeds
Running --> Failed: task returns Err
Completed --> [*]: scheduler.results()
Failed --> [*]: scheduler.results()
Pending --> Pending: ❌ can't execute directly
Completed --> Running: ❌ can't re-run
步骤 1:定义任务类型
从 typestate 标记和通用 Task 开始:
#![allow(unused)]
fn main() {
use std::marker::PhantomData;
// --- 状态标记(零大小)---
struct Pending;
struct Running;
struct Completed;
struct Failed;
// --- 任务 ID(用于类型安全的 newtype)---
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct TaskId(u64);
// --- Task 结构体,按生命周期状态参数化 ---
struct Task<State, R> {
id: TaskId,
name: String,
_state: PhantomData<State>,
_result: PhantomData<R>,
}
}
你的工作:实现状态转换,使得:
Task<Pending, R>可以转换为Task<Running, R>(通过start())Task<Running, R>可以转换为Task<Completed, R>或Task<Failed, R>- 没有其他转换可以编译
💡 提示
每个转换方法应该消费 self 并返回新状态:
#![allow(unused)]
fn main() {
impl<R> Task<Pending, R> {
fn start(self) -> Task<Running, R> {
Task {
id: self.id,
name: self.name,
_state: PhantomData,
_result: PhantomData,
}
}
}
}
步骤 2:定义工作函数
任务需要执行的函数。使用 boxed 闭包:
#![allow(unused)]
fn main() {
struct WorkItem<R: Send + 'static> {
id: TaskId,
name: String,
work: Box<dyn FnOnce() -> Result<R, String> + Send>,
}
}
你的工作:实现 WorkItem::new() 接受任务名称和闭包。
添加 TaskId 生成器(简单的原子计数器或 mutex 保护的计数器)。
步骤 3:错误处理
使用 thiserror 定义调度器的错误类型:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SchedulerError {
#[error("scheduler is shut down")]
ShutDown,
#[error("task {0:?} failed: {1}")]
TaskFailed(TaskId, String),
#[error("channel send error")]
ChannelError(#[from] std::sync::mpsc::SendError<()>),
#[error("worker panicked")]
WorkerPanic,
}
步骤 4:调度器
使用通道(第 5 章)和作用域线程(第 6 章)构建调度器:
#![allow(unused)]
fn main() {
use std::sync::mpsc;
struct Scheduler<R: Send + 'static> {
sender: Option<mpsc::Sender<WorkItem<R>>>,
results: mpsc::Receiver<TaskResult<R>>,
num_workers: usize,
}
struct TaskResult<R> {
id: TaskId,
name: String,
outcome: Result<R, String>,
}
}
你的工作:实现:
Scheduler::new(num_workers: usize) -> Self—— 创建通道并生成 workersScheduler::submit(&self, item: WorkItem<R>) -> Result<TaskId, SchedulerError>Scheduler::shutdown(self) -> Vec<TaskResult<R>>—— drop sender,加入 workers,收集结果
💡 提示 —— Worker 循环
#![allow(unused)]
fn main() {
fn worker_loop<R: Send + 'static>(
rx: std::sync::Arc<std::sync::Mutex<mpsc::Receiver<WorkItem<R>>>>,
result_tx: mpsc::Sender<TaskResult<R>>,
worker_id: usize,
) {
loop {
let item = {
let rx = rx.lock().unwrap();
rx.recv()
};
match item {
Ok(work_item) => {
let outcome = (work_item.work)();
let _ = result_tx.send(TaskResult {
id: work_item.id,
name: work_item.name,
outcome,
});
}
Err(_) => break, // 通道关闭
}
}
}
}
步骤 5:集成测试
编写测试验证:
- Happy path:提交 10 个任务,关闭,验证所有 10 个结果都是
Ok - 错误处理:提交失败的任务,验证
TaskResult.outcome是Err - 空调度器:创建并立即关闭 —— 无 panic
- 属性测试(bonus):使用
proptest验证对于任意 N 个任务(1..100),调度器总是返回恰好 N 个结果
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn happy_path() {
let scheduler = Scheduler::<String>::new(4);
for i in 0..10 {
let item = WorkItem::new(
format!("task-{i}"),
move || Ok(format!("result-{i}")),
);
scheduler.submit(item).unwrap();
}
let results = scheduler.shutdown();
assert_eq!(results.len(), 10);
for r in &results {
assert!(r.outcome.is_ok());
}
}
#[test]
fn handles_failures() {
let scheduler = Scheduler::<String>::new(2);
scheduler.submit(WorkItem::new("good", || Ok("ok".into()))).unwrap();
scheduler.submit(WorkItem::new("bad", || Err("boom".into()))).unwrap();
let results = scheduler.shutdown();
assert_eq!(results.len(), 2);
let failures: Vec<_> = results.iter()
.filter(|r| r.outcome.is_err())
.collect();
assert_eq!(failures.len(), 1);
}
}
}
步骤 6:整合
这是演示完整系统的 main():
fn main() {
let scheduler = Scheduler::<String>::new(4);
// 提交具有不同工作负载的任务
for i in 0..20 {
let item = WorkItem::new(
format!("compute-{i}"),
move || {
// 模拟工作
std::thread::sleep(std::time::Duration::from_millis(10));
if i % 7 == 0 {
Err(format!("task {i} hit a simulated error"))
} else {
Ok(format!("task {i} completed with value {}", i * i))
}
},
);
// 注意:.unwrap() 用于简洁 —— 在生产环境中处理 SendError。
scheduler.submit(item).unwrap();
}
println!("All tasks submitted. Shutting down...");
let results = scheduler.shutdown();
let (ok, err): (Vec<_>, Vec<_>) = results.iter()
.partition(|r| r.outcome.is_ok());
println!("\n✅ Succeeded: {}", ok.len());
for r in &ok {
println!(" {} → {}", r.name, r.outcome.as_ref().unwrap());
}
println!("\n❌ Failed: {}", err.len());
for r in &err {
println!(" {} → {}", r.name, r.outcome.as_ref().unwrap_err());
}
}
评估标准
| 标准 | 目标 |
|---|---|
| 类型安全 | 无效状态转换无法编译 |
| 并发 | Workers 并行运行,无数据竞争 |
| 错误处理 | 所有失败都捕获在 TaskResult 中,无 panic |
| 测试 | 至少 3 个测试;bonus 给 proptest |
| 代码组织 | 清晰的模块结构,公共 API 使用验证类型 |
| 文档 | 关键类型有文档注释解释不变量 |
扩展想法
一旦基本调度器工作,尝试这些增强:
- 优先级队列:添加
Prioritynewtype(1-10)并优先处理更高优先级的任务 - 重试策略:失败的任务重试最多 N 次,然后标记为永久失败
- 取消:添加
cancel(TaskId)方法移除待处理任务 - Async 版本:使用
tokio::sync::mpsc通道移植到tokio::spawn(第 15 章) - 指标:跟踪每个 worker 的任务计数、平均执行时间和失败率