Rust Patterns 学习笔记
基于 Rust Unofficial Patterns 的学习笔记
原文版本:
main@f279f35(2026-03-29 查验)
📖 关于
本笔记记录笔者学习 Rust Design Patterns 的理解和心得,包括:
- ✅ 认同的观点 - 原文的核心思想和最佳实践
- 💡 补充示例 - 笔者觉得更好的代码示例
- ⚠️ 批评意见 - 笔者认为不严谨或有误的地方(标注为
💭 笔记作者观点)
📚 笔记导航
Idioms (习惯用法)
- 借用优先:参数用
&T,避免不必要的所有权转移 - format! 宏:字符串拼接首选
- Default trait:无参构造的最佳实践
- RAII:同步资源用 Drop,async 资源用显式 close
- mem 工具:
replace/take用于所有权交换
Design Patterns (设计模式)
- Newtype:类型安全和显式保证
- RAII Guards:作用域结束时自动释放资源
- Builder:多参数构造用 Builder,复杂场景用 bon crate
- Visitor:封装操作异构对象的算法
- Contain unsafety:unsafe 代码集中到小模块
Anti-Patterns (反模式)
- 借用优先于 clone:clone 应该是有意为之
- 警告管理:不用
#![deny(warnings)]blanket 禁止 - Deref 正确用途:只用于智能指针,不模拟继承
Functional Programming (函数式编程)
- 命令式 vs 声明式:Rust 鼓励声明式
- 泛型单体化:编译时为每个类型生成独立代码
- Serde 三层架构:Deserialize + Visitor + Deserializer
Supplementary (补充资源)
- 重构原则:先设计再重构,无测试不重构,拆分环节设置检查点
- SOLID 原则:Rust 中的新理解(LSP = trait 契约一致性)
- 其他设计原则:18 个原则的 Rust 体现和实践
🔗 相关链接
- 原书: Rust Unofficial Patterns
- GitHub: rust-unofficial/patterns
- Rust API Guidelines: https://rust-lang.github.io/api-guidelines
Rust Idioms 学习笔记
原文版本:
main@f279f35(2026-03-29 查验)
概述
Idioms 是 Rust 社区的编码规范和最佳实践,代表“地道的 Rust 写法“。除非有充分理由,否则应遵循这些习惯。
共 15 个小节
2.1 Use borrowed types for arguments
核心原则
函数只读取数据时用借用 (&T),需要存储/消耗时用所有权。
代码对比
#![allow(unused)]
fn main() {
// ❌ 获取所有权 - 调用者无法继续使用
fn print_name(name: String) {
println!("Name: {}", name);
}
let my_name = String::from("Alice");
print_name(my_name);
// println!("{}", my_name); // ❌ 编译错误
// ✅ 借用 - 调用者可继续使用
fn print_name(name: &str) {
println!("Name: {}", name);
}
let my_name = String::from("Alice");
print_name(&my_name);
println!("{}", my_name); // ✅ 可以继续使用
}
进阶:使用 AsRef 提高通用性
#![allow(unused)]
fn main() {
fn process_path<P: AsRef<std::path::Path>>(path: P) {
let path_ref = path.as_ref();
}
}
推荐:只读参数用 &T/&str;需要存储用所有权 T;通用 API 用 AsRef<T>
2.2 Concatenating Strings with format!
核心原则
优先使用 format! 宏进行字符串拼接和格式化。
代码对比
#![allow(unused)]
fn main() {
// ❌ 使用 + 操作符 - 需要 clone,不优雅
let s = "Hello".to_string() + ", " + &name;
// ✅ 使用 format! - 清晰简洁
let s = format!("Hello, {}!", name);
// ✅ 多行字符串 - indoc crate
use indoc::indoc;
let sql = indoc! {r#"
SELECT * FROM users
WHERE id = $1
"#};
}
推荐:字符串插值用 format!;多行文本用 indoc!
2.3 Constructor
核心原则
构造函数命名遵循约定,无参构造优先用 Default。
构造函数模式
| 模式 | 用法 | 适用场景 |
|---|---|---|
new() | T::new(args) | 标准构造函数 |
default() | T::default() | 使用 Default trait |
from() | T::from(other) | 类型转换(Into/From trait) |
with_xxx() | T::with_name(x) | 变体构造函数 |
builder() | T::builder().x().y().build() | 复杂对象构建 |
实践讨论
- 无参数 + 非 async → 不定义
new(),只实现Default::default()(Clippy 推荐) - 参数多 → 使用
boncrate 的 Builder 宏
#![allow(unused)]
fn main() {
// ✅ 无参构造 - 只用 Default
#[derive(Default)]
struct Config {
value: u32,
}
// ✅ 多参数 - bon crate
use bon::Builder;
#[derive(Builder)]
struct User {
name: String,
#[builder(default = 18)]
age: u32,
email: Option<String>,
}
}
2.4 The Default Trait
核心原则
优先 derive Default,需要自定义时手动实现。
实现方式
#![allow(unused)]
fn main() {
// ✅ 自动派生(字段都有 Default)
#[derive(Default)]
struct Config {
host: String, // ""
port: u16, // 0
}
// ✅ 手动实现(自定义默认值)
impl Default for Config {
fn default() -> Self {
Self {
host: String::from("localhost"),
port: 8080,
}
}
}
// ✅ 使用 ..Default::default()
let config = Config {
host: String::from("example.com"),
..Default::default()
};
}
推荐:字段都有 Default 用 #[derive(Default)];自定义默认值手动实现 Default
2.5 Collections Are Smart Pointers
核心原则
部分集合类型(Vec, String)实现了 Deref trait,像智能指针一样自动解引用到借用视图。
关键理解
#![allow(unused)]
fn main() {
// ✅ Vec 实现了 Deref<Target=[T]>,自动解引用到 slice
let v: Vec<i32> = vec![1, 2, 3];
let s: &[i32] = &v; // 自动 Deref: &Vec → &[T]
v.len(); // 实际调用的是 <[T]>::len()
// ✅ String 实现了 Deref<Target=str>,自动解引用到 &str
let s: String = String::from("hello");
let slice: &str = &s; // 自动 Deref: &String → &str
// ❌ HashMap 没有实现 Deref,不能自动解引用
use std::collections::HashMap;
let mut map: HashMap<String, i32> = HashMap::new();
// &map 仍然是 &HashMap,没有借用视图类型
}
类型对比
| 类型 | Deref 实现 | 借用视图 | 智能指针行为 |
|---|---|---|---|
Vec<T> | ✅ Deref<Target=[T]> | &[T] (slice) | ✅ 自动解引用 |
String | ✅ Deref<Target=str> | &str | ✅ 自动解引用 |
HashMap<K,V> | ❌ | 无 | ❌ 仅普通借用 |
Box<T> | ✅ Deref<Target=T> | &T | ✅ 智能指针 |
智能指针集合:Vec, String 实现 Deref,自动解引用到借用视图;非智能指针集合:HashMap, BTreeMap 等没有 Deref;API 设计:方法通常实现在借用视图上
2.6 Finalisation in Destructors
核心原则
利用 Drop trait 实现 RAII,自动清理资源。
RAII 模式
// ✅ 经典例子:文件自动关闭
struct FileHandle {
fd: i32,
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("Closing file descriptor {}", self.fd);
}
}
fn main() {
let file = FileHandle { fd: 42 };
} // ← drop() 自动调用
async 资源清理
#![allow(unused)]
fn main() {
// ✅ async 资源 - 显式 close 方法
struct AsyncResource {
conn: Connection,
}
impl AsyncResource {
async fn close(self) -> Result<()> {
self.conn.close().await
}
}
// 使用
resource.close().await?;
// 保护机制:测试 + Code Review
}
推荐:同步资源用 RAII + Drop;async 清理用显式 close() 方法;Drop 中不要 panic
2.7 mem::{take, replace} - 所有权交换
核心原则
不拷贝地替换值,实现所有权交换。
💡 比喻:像印第安纳琼斯用沙袋替换神器——拿走原物,留下替代品。
函数对比
| 函数 | 作用 | 等价于 |
|---|---|---|
mem::replace(&mut a, b) | 用 b 替换 a,返回旧 a | - |
mem::take(&mut a) | 拿走 a,留下 default() | replace(&mut a, Default::default()) |
Option::take() | 拿走 Option,留下 None | replace(&mut opt, None) |
常见场景
#![allow(unused)]
fn main() {
// 1. 状态机中拿走状态
let data = mem::take(&mut self.data);
// 2. Option 取值
let value = opt.take().unwrap_or_default();
// 3. 重新配置时替换
let old = mem::replace(&mut self.config, new_config);
}
进阶场景:Enum 变体切换
原文核心用例:在 enum 变体之间切换时,不 clone 地提取字段。
#![allow(unused)]
fn main() {
use std::mem;
enum MyEnum {
A { name: String, x: u8 },
B { name: String },
}
// A → B 变体切换,不 clone name
fn a_to_b(e: &mut MyEnum) {
if let MyEnum::A { name, x: 0 } = e {
*e = MyEnum::B {
name: mem::take(name), // 拿走 name,留下空 String
}
}
}
}
为什么需要:借用检查器不允许直接从 &mut 中拿走值(必须有东西占位),mem::take 用默认值占位,返回所有权。
关系图
mem::replace ← 最底层
↑
│
┌───┴───┐
│ │
mem::take Option::take()
所有权交换,避免 clone;常见场景:状态机、Option 取值、配置替换;enum 变体切换
2.8 On-Stack Dynamic Dispatch
核心原则
临时批量处理时用栈上借用,避免堆分配。
代码对比
#![allow(unused)]
fn main() {
// ❌ 堆上动态分发
let traits: Vec<Box<dyn Trait>> = vec![
Box::new(A),
Box::new(B),
];
// ✅ 栈上动态分发
let a = A;
let b = B;
let traits: [&dyn Trait; 2] = [&a, &b];
}
推荐:临时批量用 &[&dyn Trait] 栈上分发;需要所有权用 Box<dyn Trait>
2.9 Foreign function interface (FFI)
核心原则
安全地与 C 等语言交互。
关键要点
#![allow(unused)]
fn main() {
// 1. 字符串转换
use std::ffi::CString;
let rust_str = "hello";
let c_str = CString::new(rust_str).unwrap();
let c_ptr = c_str.as_ptr(); // *const i8
// 2. 错误处理
// C 的 errno → Rust 的 Result
}
字符串转换用 CString/CStr;错误处理包装为 Result
2.10 Iterating over an Option
核心原则
Option 可以当迭代器用。
代码示例
#![allow(unused)]
fn main() {
// ✅ Some 时执行一次,None 时不执行
for x in Some(5) {
println!("{}", x);
}
// ✅ 链式调用
let result: Vec<i32> = Some(5)
.into_iter()
.map(|x| x * 2)
.collect(); // [10]
}
Option<T> 实现 IntoIterator;Some 迭代一次,None 跳过;避免模式匹配嵌套
2.11 Pass Variables to Closure
核心原则
理解闭包捕获变量的方式。
捕获方式
#![allow(unused)]
fn main() {
// 1. 借用捕获(默认)
let x = 5;
let closure = || println!("{}", x);
// 2. move 捕获
let x = vec![1, 2, 3];
let closure = move || println!("{:?}", x);
// 3. 可变借用
let mut x = 5;
let mut closure = || x += 1;
}
借用捕获:只读访问;move 捕获:转移所有权(多线程、返回闭包);可变借用:修改捕获变量
2.12 Privacy For Extensibility
核心原则
用隐私控制实现可扩展的 API。
代码示例
#![allow(unused)]
fn main() {
pub struct Config {
inner: InnerConfig, // 私有字段
}
impl Config {
pub fn new() -> Self { } // 公开构造方法
}
}
私有字段 + 公共方法,保留未来修改内部实现的权利,避免锁定实现细节
2.13 Easy doc Initialization
核心原则
使用 ..Default::default() 简化初始化。
代码示例
#![allow(unused)]
fn main() {
let config = Config {
name: String::from("test"),
port: 8080,
..Default::default()
};
}
..Default::default() 填充未指定字段,只关注需要自定义的部分
2.14 Temporary mutability
核心原则
让可变性尽可能局部。
代码示例
#![allow(unused)]
fn main() {
let mut buffer = String::new();
buffer.push_str("hello");
buffer.push_str(" world");
let result = buffer; // 之后不再需要 mut
}
可变性限制在最小作用域,构建完成后转为不可变,减少借用冲突
2.15 Return consumed arg on error
核心原则
错误时返回原始输入,让调用者决定如何处理。
代码示例
#![allow(unused)]
fn main() {
fn parse(s: String) -> Result<i32, String> {
s.parse().map_err(|_| s) // 返回原字符串
}
}
错误时返回原始输入,调用者可重试或记录,而非直接丢弃
本部分学习总结
核心收获
- 借用优先 - 参数用
&T,避免不必要的所有权转移 - format! 宏 - 字符串拼接首选
- Default trait - 无参构造的最佳实践
- Move 语义 - 集合类型赋值会转移所有权
- RAII - 同步资源用 Drop,async 资源用显式 close
- mem 工具 -
replace/take用于所有权交换 - 栈上分发 - 临时批量处理避免堆分配
最佳实践
| 场景 | 推荐做法 |
|---|---|
| 函数参数 | &T 或 AsRef<T> |
| 字符串 | format! / indoc |
| 构造 | new() / Default / bon |
| 资源清理 | RAII / 显式 close() |
| 集合传参 | &Vec / &HashMap |
常见陷阱
- ❌ 忘记集合是 Move 语义
- ❌ async 资源在 Drop 中无法清理
- ❌ 返回栈上的 trait object 引用
Rust Design Patterns 学习笔记
原文版本:
main@f279f35(2026-03-29 查验)部分:Part 2 - Design Patterns (设计模式)
概述
设计模式是解决常见编程问题的经典方法。本章分为 4 个类别:
- Behavioural (行为型): 对象间的通信和职责分配
- Creational (创建型): 对象的创建机制
- Structural (结构型): 如何组合类和结构体
- FFI: 与其他语言交互的模式
共 14 个小节
目录
Behavioural Patterns
Creational Patterns
Structural Patterns
- 3.3.1 Compose Structs
- 3.3.2 Prefer Small Crates
- 3.3.3 Contain unsafety in small modules
- 3.3.4 Avoid complex type bounds with custom traits
FFI Patterns
Behavioural Patterns
3.1.1 Command
核心:把请求封装成对象,Rust 中用 trait 或闭包实现。
代码示例:
#![allow(unused)]
fn main() {
// trait 方式
trait Command {
fn execute(&self);
}
let cmds: Vec<Box<dyn Command>> = vec![...];
// 闭包方式(更 Rust)
type Cmd = Box<dyn Fn()>;
let cmds: Vec<Cmd> = vec![...];
}
应用:任务队列、撤销/重做、宏录制
3.1.2 Interpreter
核心:用枚举 + 递归实现 DSL 解释器。
代码示例:
#![allow(unused)]
fn main() {
// 枚举 + 递归实现 DSL
enum Expr {
Number(i32),
Add(Box<Expr>, Box<Expr>),
// ...
}
impl Expr {
fn interpret(&self) -> i32 {
match self { /* 递归求值 */ }
}
}
}
循环引用处理方案:
| 方案 | 适用场景 |
|---|---|
Box | 小型 DSL,简单递归 |
| 索引 + Arena | 大型 AST,需要序列化 |
| 静态检查禁止循环 | 配置类 DSL,规则引擎 |
| 索引 + 两阶段构建 | 复杂 DSL,类型系统 |
3.1.3 Newtype
核心:用元组结构体包装类型,获得类型安全和自定义能力。
代码示例:
#![allow(unused)]
fn main() {
// 类型安全
struct UserId(u32);
struct OrderId(u32);
// 不能混用!
// 为外部类型实现 trait
struct CommaSeparated(Vec<String>);
impl Display for CommaSeparated { }
// 封装验证
struct Email(String);
Email::new("test@example.com")?; // 创建即保证有效
}
使用场景:
- 防止类型混淆(UserId vs OrderId)
- 为外部类型实现 trait
- 封装验证逻辑(类型保证有效性)
- 语义清晰
- 隔离不同领域
3.1.4 RAII Guards
核心:用守卫对象在作用域结束时自动释放资源。
代码示例:
#![allow(unused)]
fn main() {
// 并发保护
let guard = mutex.lock(); // 自动解锁
// 运行时借用检查
let borrow = refcell.borrow(); // 运行时验证借用规则
// 作用域清理
let guard = ScopeGuard::new(|| cleanup());
}
Guard 的三层含义:
- 并发保护:
MutexGuard - 借用检查:
Ref/RefMut(运行时借用验证) - 作用域清理:
ScopeGuard
3.1.5 Strategy
核心:定义可互换的算法族,Rust 中用 trait 实现。
代码示例:
#![allow(unused)]
fn main() {
// 用户推荐做法:为闭包实现 trait
trait Strategy {
fn execute(&self);
}
// 为具体类型实现
struct MyStrategy;
impl Strategy for MyStrategy { }
// 为闭包实现
impl<F: Fn()> Strategy for F {
fn execute(&self) { self() }
}
// 使用:两种方式都可以
fn run_strategy(s: &dyn Strategy) {
s.execute();
}
run_strategy(&MyStrategy);
run_strategy(&|| println!("Hello"));
}
实现方式对比:
| 方式 | 优点 | 缺点 |
|---|---|---|
| trait 对象 | 运行时切换 | 虚表调用 |
| 泛型 | 性能优,内联 | 编译时确定 |
| 闭包 + trait | 两者兼得 | 需要额外定义 |
3.1.6 Visitor
核心:Visitor 封装了一个操作异构对象集合的算法,可以在不修改数据的情况下添加新算法。
代码示例:
#![allow(unused)]
fn main() {
// 1. 数据结构 (AST)
pub enum Expr {
IntLit(i64),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
}
// 2. 抽象 Visitor trait
pub trait Visitor<T> {
fn visit_expr(&mut self, e: &Expr) -> T;
}
// 3. 具体 Visitor (解释器)
struct Interpreter;
impl Visitor<i64> for Interpreter {
fn visit_expr(&mut self, e: &Expr) -> i64 {
match *e {
Expr::IntLit(n) => n,
Expr::Add(ref lhs, ref rhs) =>
self.visit_expr(lhs) + self.visit_expr(rhs),
Expr::Sub(ref lhs, ref rhs) =>
self.visit_expr(lhs) - self.visit_expr(rhs),
}
}
}
// 4. 可选:walk_* 函数复用遍历逻辑
pub fn walk_expr<V: Visitor<i64>>(visitor: &mut V, e: &Expr) {
match *e {
Expr::Add(ref lhs, ref rhs) => {
visitor.visit_expr(lhs);
visitor.visit_expr(rhs);
}
// ...
}
}
}
核心价值:
- 复用遍历逻辑
- 减少样板代码
- 解耦数据与算法
- 切面插入(调试/监控)
Visitor 组合用法:
#![allow(unused)]
fn main() {
// 1. 链式处理(Pipeline)
let ast = TypeChecker.visit(ast);
let ast = Optimizer.visit(ast);
let code = CodeGen.visit(ast);
// 2. 包装器(Decorator)
let visitor = LoggingVisitor {
inner: CachedVisitor {
inner: Optimizer,
}
};
// 3. serde 中的 Visitor
impl<'de> Visitor<'de> for MyTypeVisitor {
type Value = MyType;
fn visit_map<V>(self, mut map: V) -> Result<MyType, V::Error> { }
}
}
适用:异构数据 + 多算法;Rust 实现:trait + walk_*;主要场景:serde 等序列化库
Creational Patterns
3.2.1 Builder
核心:用 builder helper 构造对象,解决 Rust 无构造函数重载/默认参数的问题。
代码示例:
#![allow(unused)]
fn main() {
// 简单:普通构造方法
impl Foo {
fn new(bar: String) -> Self {
Foo { bar }
}
}
// 中等:手写 Builder
struct FooBuilder { bar: Option<String> }
impl FooBuilder {
fn bar(mut self, b: String) -> Self {
self.bar = Some(b);
self
}
fn build(self) -> Foo {
Foo { bar: self.bar.unwrap() }
}
}
// 复杂:bon crate(推荐)
use bon::Builder;
#[derive(Builder)]
struct Foo {
bar: String,
#[builder(default = 42)]
count: u32,
}
}
使用场景分级:
| 复杂度 | 方案 |
|---|---|
| 简单 | 普通 new() 方法 |
| 中等 | 手写 Builder |
| 复杂 | bon crate |
3.2.2 Fold
核心:对 AST 递归转换,创建新数据结构。
重要区分:Iterator::fold() ≠ Fold 模式
#![allow(unused)]
fn main() {
// Iterator::fold() - 聚合成单个值(和 Fold 模式无关!)
let sum = vec![1, 2, 3].iter().fold(0, |acc, x| acc + x); // 6
// Fold 模式 - 数据结构转换
let hir_ast = folder.fold_expr(ast_expr); // 新 AST
}
代码示例:
#![allow(unused)]
fn main() {
// 实际场景:常量折叠
struct ConstantFolder;
impl Folder for ConstantFolder {
fn fold_expr(&mut self, e: Box<Expr>) -> Box<Expr> {
match *e {
// 1 + 2 → 3
Expr::Binary(Op::Add,
Box::new(Expr::Int(a)),
Box::new(Expr::Int(b))) => {
Box::new(Expr::Int(a + b))
}
_ => e,
}
}
}
// syn 库(过程宏)用类似模式
use syn::visit_mut::{self, VisitMut};
impl VisitMut for MyTransform {
fn visit_expr_mut(&mut self, e: &mut Expr) { }
}
}
实际场景:
- 编译器 AST → HIR 转换
- 宏展开
- 常量折叠优化
Structural Patterns
3.3.1 Compose Structs
核心:将大结构体拆分成多个小结构体再组合,实现独立借用字段。
⚠️ 原文示例问题:
#![allow(unused)]
fn main() {
// 原文示例:拆分后函数签名也变了
fn print_database(
connection_str: ConnectionString, // 不再是 &Database
timeout: Timeout,
pool_size: PoolSize,
) { }
}
💭 笔记作者观点:
- 通过改变函数签名来“解决“问题,不是真正的解决方案
- 如果函数参数仍是
&Database,借用冲突依然存在- 本质是借用检查的基本技巧,不是设计模式
更实用的做法:
#![allow(unused)]
fn main() {
// ✅ 更地道的做法:destructuring
fn process(db: &mut Database) {
let Database { connection_string, timeout, pool_size } = db;
// 或直接用 &db.field1, &db.field2
}
// ✅ Rust 本身支持字段独立借用
let x = &mut d.a;
let y = &d.b; // 完全可以
}
真正需要拆分的场景:
- 需要为字段实现不同 trait
- 类型安全(Newtype)
- 模块封装需求
- 字段本身有复杂逻辑
3.3.2 Prefer Small Crates
核心:优先使用小而专注的 crate,每个 crate 做好一件事。
优势:
- 小 crate 更易理解,鼓励模块化
- 支持跨项目复用
- 多 crate 可并行编译
劣势:
- 依赖地狱:版本冲突(如
url:1.0vsurl:0.5类型不兼容) - 缺乏审核:crates.io 不审核,可能质量差或恶意
- 性能损失:默认无 LTO,优化差
用户实践原则:如无必要则不拆,要拆则精心设计
-
不主动拆分:
- 避免过度工程化
- 减少依赖管理复杂度
-
需要拆分时:
- 做好完善的设计(清晰的 API 边界)
- 完善的测试(减少版本冲突风险)
- 考虑语义化版本(semver)兼容性
-
拆分时机:
- 功能真正独立且可复用
- 有明确的责任边界
- 可能被其他项目使用
示例:
// 好的拆分:
my-lib/
├── Cargo.toml
├── my-lib-core/ # 核心逻辑,独立
├── my-lib-http/ # HTTP 相关,可选
└── my-lib-cli/ # CLI 工具,可选
// 避免的拆分:
my-lib/
├── Cargo.toml
├── my-lib-types/ # 只有几个类型定义 ❌
├── my-lib-utils/ # 只有几个工具函数 ❌
└── my-lib/ # 主 crate
原则:如无必要则不拆,要拆则完善设计 + 测试,减少版本冲突风险
3.3.3 Contain unsafety in small modules
核心:将 unsafe 代码限制在尽可能小的模块内,构建最小的安全接口。
代码示例:
#![allow(unused)]
fn main() {
// 内层:最小 unsafe 模块
mod unsafe_impl {
pub struct RawBuffer { ptr: *mut u8, len: usize }
impl RawBuffer {
pub unsafe fn new(ptr: *mut u8, len: usize) -> Self { }
pub unsafe fn get(&self, idx: usize) -> u8 { }
}
}
// 外层:安全接口
pub mod safe_api {
pub struct SafeBuffer { inner: RawBuffer }
impl SafeBuffer {
pub fn new(data: Vec<u8>) -> Self { }
pub fn get(&self, idx: usize) -> Option<u8> { }
}
}
}
用户观点:
- 这是 Rust 本身提供两套语法(safe/unsafe)而产生的模式
- 要点:
- unsafe 代码集中起来
- 集中检查和提供安全保证
- 明确安全和不安全的边界
- 同时通过 unsafe 提供足够的底层掌控和性能优化空间
场景:FFI/原始指针/并发原语;标准库例子:Vec/String/Cell/Mutex
3.3.4 Avoid complex type bounds with custom traits
核心:当 trait bounds 过于复杂时,引入新 trait 来简化。
代码示例:
#![allow(unused)]
fn main() {
// ❌ 复杂 bounds
struct Value<
G: FnMut() -> Result<T, Error>,
S: Fn(&T) -> Status,
T: Display
> { /* ... */ }
// ✅ 用 trait 简化
trait Getter {
type Output: Display;
fn get_value(&mut self) -> Result<Self::Output, Error>;
}
impl<F: FnMut() -> Result<T, Error>, T: Display> Getter for F {
type Output = T;
fn get_value(&mut self) -> Result<Self::Output, Error> { self() }
}
struct Value<G: Getter, S: Fn(&G::Output) -> Status> { /* ... */ }
}
核心价值:
- 类型参数:3 个 → 2 个
- 可读性:
FnMut() -> Result<T, Error>→Getter - 类型擦除:容易 (
Box<dyn Getter>)
用户观点:用一个 trait 来把复杂的 trait bound 中若干约束集中到一起
FFI Patterns
3.4.1 Object-Based APIs
核心:设计 FFI API 时,采用“基于对象的 API“模式,明确所有权和生命周期边界。
设计原则:
| 类型 | 所有权 | 管理 | 可见性 |
|---|---|---|---|
| Encapsulated | Rust 拥有 | 用户管理 | 不透明 |
| Transactional | 用户拥有 | 用户管理 | 透明 |
例子: POSIX DBM API
// C 语言视角
struct DBM; // 不透明类型
typedef struct { void *dptr; size_t dsize; } datum; // 透明类型
DBM* dbm_open(...); // Rust 拥有
datum dbm_firstkey(DBM*); // 生命周期绑定
void dbm_close(DBM*);
代码示例:
#![allow(unused)]
fn main() {
// Rust 内部 - safe 代码
pub struct Dbm {
data: Vec<i32>, // Rust 完全拥有
}
impl Dbm {
// ✅ 完全是 safe Rust
pub fn process(&mut self) {
for x in &mut self.data { *x *= 2; }
}
}
// FFI 边界 - 只负责转换
#[no_mangle]
pub extern "C" fn dbm_process(db: *mut Dbm) {
unsafe { (*db).process() } // unsafe 只在这一行
}
}
核心:Encapsulated(Rust 拥有) + Transactional(用户拥有);unsafe 只在边界,内部 safe
3.4.2 Type Consolidation into Wrappers
核心:将多个相关类型合并到一个“包装器类型“中,最小化内存不安全的风险。
问题背景:
- Rust 的生命周期保证内存安全
- 导出到 FFI 时,类型变成指针,生命周期信息丢失
- 用户需要管理生命周期,容易出现 use-after-free
代码示例:
#![allow(unused)]
fn main() {
// ✅ 包装器模式 - 生命周期绑定在一起
struct MySetWrapper {
myset: MySet,
iter_next: usize, // 迭代状态内嵌(只是索引,无生命周期)
}
impl MySetWrapper {
pub fn first_key(&mut self) -> Option<&Key> {
self.iter_next = 0;
self.next_key()
}
pub fn next_key(&mut self) -> Option<&Key> {
// 每次调用重新创建迭代器,从当前位置继续
if let Some(next) = self.myset.keys().nth(self.iter_next) {
self.iter_next += 1;
Some(next)
} else {
None
}
}
}
}
用户理解:
- 相当于自己实现一个跨 FFI 的没有生命周期的迭代器
- 核心技巧:把“持有引用的迭代器“变成“带状态的索引“
⚠️ 局限性(原文承认):
- 依赖
nth()的高效实现 - 对于
HashSet/HashMap等容器,每次nth()是 O(n),总体 O(n²) - 仅适用于支持随机访问的容器(如
Vec) - 原文:真正安全的实现 “incredibly difficult”
本部分学习总结
核心收获
-
类型安全思维(Newtype)
- 用类型系统把隐式假设变成显式保证
- 零开销抽象
-
资源管理思维(RAII)
- 同步资源用 Drop
- async 资源用显式 close
-
安全边界思维(Contain unsafety + FFI 模式)
- unsafe 集中到最小模块,对外提供安全接口
- FFI 中数据在 Rust 内部,unsafe 只在边界
- 把“持有引用的迭代器“变成“带状态索引的包装器“
模式质量评价
真正重要的模式:
- Newtype、RAII Guards、Contain unsafety —— 充分利用 Rust 安全特性
特定场景有用:
- Visitor、Strategy、Builder —— 经典设计模式的 Rust 实现
有争议的模式:
- Compose Structs —— 本质是借用检查基本技巧,不是设计模式
- 部分示例为了模式而模式,没有清晰对比优劣
Idioms vs Patterns 区别
| 方面 | Idioms | Patterns |
|---|---|---|
| 定位 | 日常编码习惯 | 设计模板 |
| Rust 特色 | 借用、所有权 | 类型系统、安全边界 |
| 使用频率 | 高 | 中/低 |
实践指导
| 场景 | 推荐模式 |
|---|---|
| 防止类型混淆 | Newtype |
| 资源清理 | RAII / 显式 close |
| 包装 unsafe | Contain unsafety |
| 复杂对象构建 | Builder(bon crate) |
| AST 处理 | Visitor / Fold |
| FFI 库设计 | Object-Based APIs + Type Consolidation |
Rust Anti-Patterns 学习笔记
原文版本:
main@f279f35(2026-03-29 查验)部分:Part 3 - Anti-Patterns (反模式)
概述
Anti-Patterns 是应该避免的做法。这些模式看似能解决问题,但实际上会造成更多问题。
共 3 个小节
目录
4.1 Clone to satisfy the borrow checker
核心:借用检查器报错时,用 .clone() 来让代码编译通过,但这会导致数据不同步和性能开销。
问题示例:
#![allow(unused)]
fn main() {
// ❌ 反模式:clone 了一个新值,不是原来的 x
let mut x = 5;
let y = &mut (x.clone()); // 克隆了一个新值
*y += 1; // 修改的是克隆的值,不是 x
}
后果:
- 两个变量数据不同步
- 不必要的性能开销
- 掩盖了对所有权的理解不足
例外情况(可以接受):
Rc<T>和Arc<T>的智能克隆(引用计数,非深拷贝)- 学习阶段/原型
- 性能不关键的场景
正确做法:
#![allow(unused)]
fn main() {
// ✅ 能借用时尽量借用
let mut x = 5;
let y = &mut x; // 直接借用
*y += 1;
}
clone 应该是有意为之的数据拷贝决策,能借用时尽量借用
4.2 #[deny(warnings)]
核心:在 crate 根使用 #![deny(warnings)] 来确保代码无警告编译,但这会破坏 Rust 的稳定性保证。
问题:
-
破坏 Rust 的稳定性保证:
- Rust 有时会用新的 lint 警告某些问题
- 经过一段时间后才变成
deny(硬错误) #![deny(warnings)]会立即把这些警告变成错误
-
无法灵活处理警告:
- API 废弃会发出警告
- 新版本引入新 lint 时可能编译失败
-
无法使用 clippy 等工具
替代方案:
-
命令行设置(推荐):
RUSTFLAGS="-D warnings" cargo build -
明确指定要 deny 的 lint:
#![allow(unused)] #![deny( fn main() { bad_style, dead_code, unused, // 只 deny 你确定要严格执行的 )] }
更好的做法:命令行设置或明确列出要 deny 的 lint
4.3 Deref Polymorphism
核心:滥用 Deref trait 来模拟结构体之间的“继承“,从而复用方法。
问题示例:
// ❌ 反模式:用 Deref 模拟继承
use std::ops::Deref;
struct Foo {}
impl Foo {
fn m(&self) { /* ... */ }
}
struct Bar {
f: Foo,
}
impl Deref for Bar {
type Target = Foo;
fn deref(&self) -> &Foo {
&self.f
}
}
fn main() {
let b = Bar { f: Foo {} };
b.m(); // 通过 Deref 自动解引用调用 Foo 的方法
}
为什么是反模式:
| 原因 | 说明 |
|---|---|
| 违反直觉 | Deref 设计目的是指针解引用,不是类型转换 |
| 不支持子类型 | Foo 的 trait 不会自动为 Bar 实现 |
| 语义差异 | self 指向定义方法的类型,不是实际对象 |
| 功能有限 | 只支持单“继承“,无接口/隐私等概念 |
Deref 的正确用途:
Box<T>→TRc<T>→TString→str- 自定义智能指针
替代方案:
-
手动转发(推荐):
#![allow(unused)] fn main() { impl Bar { fn m(&self) { self.f.m() // 清晰明确 } } } -
使用 trait 抽象
-
使用委托 crate:
不要用 Deref 模拟继承,应该手动转发或使用 trait/委托 crate
本部分学习总结
核心收获
-
借用优先于 clone
- clone 应该是有意为之的数据拷贝决策
- 能借用时尽量借用
-
警告管理的正确方式
- 不用
#![deny(warnings)]blanket 禁止 - 用命令行或明确列出具体 lint
- 不用
-
Deref 的正确用途
- 只用于智能指针解引用
- 不用来模拟继承
实践指导
| 场景 | 推荐做法 |
|---|---|
| 借用检查报错 | 先想借用,再考虑 clone |
| CI 严格要求 | RUSTFLAGS="-D warnings" |
| 方法复用 | 手动转发 / trait / 委托宏 |
| 智能指针 | 正确使用 Deref |
Rust Functional Programming 学习笔记
原文版本:
main@f279f35(2026-03-29 查验)部分:Part 4 - Functional Programming (函数式编程)
概述
函数式编程范式在 Rust 中的应用和实践。
共 3 个小节
目录
5.1 Programming paradigms
核心:命令式描述“如何做“,声明式描述“做什么“。
命令式示例:
#![allow(unused)]
fn main() {
let mut sum = 0;
for i in 1..11 {
sum += i;
}
println!("{sum}");
}
- 需要像编译器一样逐步跟踪状态变化
- 这是大多数人学习编程的起点
声明式示例:
#![allow(unused)]
fn main() {
println!("{}", (1..11).fold(0, |a, b| a + b));
}
fold是函数组合(来自 Haskell 的约定)- 描述“对 1 到 10 进行累加“,而非具体步骤
关键对比:
| 方面 | 命令式 | 声明式 |
|---|---|---|
| 描述 | how 如何做 | what 做什么 |
| 状态 | 显式跟踪变化 | 隐藏状态变化 |
| 思维 | 步骤序列 | 函数组合 |
| Rust 中的体现 | for/while 循环 | iter().map().fold() |
Rust 鼓励声明式:Iterator 链式调用
5.2 Generics as Type Classes
核心:Rust 的泛型设计更像函数式语言(如 Haskell),泛型参数实际上是类型类约束,不同的泛型参数会创建不同的类型。
关键概念 - Monomorphization(单体化/单态化):
- 编译时将泛型代码转换为多个具体类型的独立版本
Vec<i32>和Vec<char>是两个不同的类型- 编译器会为每个具体类型生成独立代码
- 优点:运行时无开销;缺点:代码膨胀
三种语言泛型机制对比:
| 特性 | Java | C++ | Rust |
|---|---|---|---|
| 实现机制 | 类型擦除 | 模板单体化 | 泛型单体化 |
| 运行时泛型信息 | ❌ 无 | ❌ 无 | ❌ 无 |
| 代码生成 | 1 份代码,运行时转换 | 每类型 1 份 | 每类型 1 份 |
| 二进制大小 | ✅ 小 | ❌ 大 | ❌ 大 |
| 运行效率 | ⚠️ 有转换开销 | ✅ 无间接层 | ✅ 无间接层 |
| 特化方法 | ❌ 不支持 | ✅ 模板特化 | ✅ 多 impl 块 |
| 类型安全 | 编译时 + 运行时 | 编译时 | 编译时 |
验证结论:
💭 笔记作者观点:
- 书中表述有误:将 C++ 和 Java 混为一谈,两者机制完全不同
- C++ 和 Rust 泛型本质相同:都是编译时单体化
- Java 泛型 = 类型擦除:运行时不存在泛型信息,用
Object替代- Rust 的优势:
impl块语法比 C++ 模板特化更简洁,与 trait 系统深度整合
问题示例 - 运行时 vs 编译时决策:
#![allow(unused)]
fn main() {
// ❌ 运行时决策:用枚举包装
enum AuthInfo {
Nfs(crate::nfs::AuthInfo),
Bootp(crate::bootp::AuthInfo),
}
struct FileDownloadRequest {
file_name: PathBuf,
authentication: AuthInfo,
mount_point: Option<PathBuf>, // 只有 NFS 需要
}
// 调用者必须处理 None
fn mount_point(&self) -> Option<&Path> {
self.mount_point.as_ref()
}
}
✅ 泛型解决方案 - 编译时分离 API:
#![allow(unused)]
fn main() {
// 1. 定义协议 trait
pub(crate) trait ProtoKind {
type AuthInfo;
fn auth_info(&self) -> Self::AuthInfo;
}
// 2. 具体协议类型
pub struct Nfs { /* ... */ }
impl ProtoKind for Nfs { /* ... */ }
pub struct Bootp;
impl ProtoKind for Bootp { /* ... */ }
// 3. 泛型请求类型
struct FileDownloadRequest<P: ProtoKind> {
file_name: PathBuf,
protocol: P,
}
// 4. 通用方法
impl<P: ProtoKind> FileDownloadRequest<P> {
fn file_path(&self) -> &Path { &self.file_name }
fn auth_info(&self) -> P::AuthInfo { self.protocol.auth_info() }
}
// 5. 特定协议方法
impl FileDownloadRequest<Nfs> {
fn mount_point(&self) -> &Path { self.protocol.mount_point() }
}
}
使用效果:
#![allow(unused)]
fn main() {
// ❌ 编译错误:Bootp 没有 mount_point() 方法
let request: FileDownloadRequest<Bootp> = ...;
request.mount_point(); // ❌ 编译错误!
}
优势:
- 编译时错误检测,而非运行时检查
- 共享字段去重
- impl 块按状态组织,更清晰
劣势:
- 增加二进制大小(单体化导致)
实际应用场景:
| 场景 | 例子 |
|---|---|
| 标准库 | Vec<u8> 可从 CString 转换 |
| 嵌入式 | embedded-hal 静态验证引脚配置 |
| HTTP | hyper 不同连接器有不同方法 |
| Type State 模式 | 对象基于内部状态获得/失去 API |
替代方案:
- 需要“分割 API“ → 考虑 Builder Pattern
- API 相同仅行为不同 → 考虑 Strategy Pattern
泛型 = 类型类约束,不同参数 = 不同类型;优势是编译时检查,代价是二进制大小增加
5.3 Functional Optics
核心:Optics(光学/透镜)是函数式语言的 API 设计模式,用于组合行为和属性。Rust 不直接支持 Optics,但这个概念有助于理解某些 API(如 Serde)。
三种 Optics
1. The Iso(同构)
最简单的值转换器,一对函数在两个类型间转换。
#![allow(unused)]
fn main() {
struct ConcordanceSerde {}
impl ConcordanceSerde {
fn serialize(value: Concordance) -> String { /* A → B */ }
fn deserialize(value: String) -> Concordance { /* B → A */ }
}
}
特点:两个固定类型之间的转换。
2. The Poly Iso(多态同构)
允许泛型类型,返回单一类型。
#![allow(unused)]
fn main() {
// 标准库中的例子
pub trait FromStr: Sized {
type Err;
fn from_str(s: &str) -> Result<Self, Self::Err>;
}
pub trait ToString {
fn to_string(&self) -> String;
}
}
问题:
to_string不指明格式(JSON? XML?)- 每个类型都要手写实现,难以扩展
3. The Prism(棱镜)
“更高一层“的泛型,支持多种格式。
Serde[T, F]:
serialize: T, F -> String
deserialize: String, F -> Result[T, Error]
T= 数据类型(User, Product, Order…)F= 格式类型(JSON, YAML, XML, CBOR…)
Serde 的三层架构
| 层级 | Trait | 职责 | 谁实现 |
|---|---|---|---|
| 顶层 | Serialize/Deserialize | 用户数据类型 | 用户(宏生成) |
| 中层 | Visitor | 构造/析构逻辑 | 宏生成 |
| 底层 | Serializer/Deserializer | 格式特定实现 | 格式库 |
┌─────────────────────────────────────┐
│ 顶层:Deserialize │ ← User::deserialize()
│ #[derive(Deserialize)] │ 创建 Visitor 并传递
│ struct User { ... } │
├─────────────────────────────────────┤
│ 中层:Visitor │ ← UserVisitor::visit_map()
│ visit_map, visit_str, visit_u64 │ 被 Deserializer 驱动
├─────────────────────────────────────┤
│ 底层:Deserializer │ ← JsonDeserializer
│ 解析 JSON 字节,调用 Visitor │ 解析格式,反向调用
└─────────────────────────────────────┘
数据访问层:两种形式
形式 1:基础类型直接传值
#![allow(unused)]
fn main() {
pub trait Visitor<'de> {
// 基础类型:直接传递值
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>;
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>;
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>;
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>;
}
}
形式 2:复杂类型用访问 trait
#![allow(unused)]
fn main() {
pub trait Visitor<'de> {
// 复杂类型:需要进一步访问,按需解析
fn visit_map<V>(self, visitor: V) -> Result<Self::Value, V::Error>
where V: MapAccess<'de>;
fn visit_seq<V>(self, visitor: V) -> Result<Self::Value, V::Error>
where V: SeqAccess<'de>;
fn visit_enum<V>(self, visitor: V) -> Result<Self::Value, V::Error>
where V: EnumAccess<'de>;
}
}
为什么有这种区分:
- 基础类型:值已解析完成,直接传递
- 复杂类型:需要进一步访问,按需解析
MapAccess 的作用
为什么需要 MapAccess:
| 问题 | 没有 MapAccess | 有 MapAccess |
|---|---|---|
| 耦合度 | Visitor 绑定具体 Deserializer | Visitor 只依赖 trait |
| 实现次数 | N 类型 × M 格式 = N×M 次 | N 类型 + M 格式 = N+M 次 |
| 解析方式 | 一次性加载所有数据 | 按需解析,流式支持 |
MapAccess trait 定义:
#![allow(unused)]
fn main() {
pub trait MapAccess<'de> {
type Error: Error;
// 方法级泛型:每次调用可以是不同类型
fn next_key<K>(&mut self) -> Result<Option<K>, Self::Error>
where K: Deserialize<'de>;
fn next_value<V>(&mut self) -> Result<V, Self::Error>
where V: Deserialize<'de>;
}
}
类型约束关系:
| 类型参数 | 级别 | 绑定到 | 决定方 |
|---|---|---|---|
'de | trait 级 | MapAccess<'de> | Deserializer |
type Error | 关联类型 | MapAccess | Deserializer |
V (visit_map) | 方法级 | visit_map | Deserializer |
K (next_key) | 方法级 | next_key | Visitor |
V (next_value) | 方法级 | next_value | Visitor |
具体实现:
| 格式库 | V 的具体类型 |
|---|---|
| serde_json | JsonMapAccess<'de> |
| serde_yaml | YamlMapAccess<'de> |
| serde_cbor | CborMapAccess<'de> |
| serde_xml | XmlMapAccess<'de> |
完整交互流程
#![allow(unused)]
fn main() {
// 用户代码
let user: User = serde_json::from_str(json)?;
// 内部调用序列:
// 1. from_str 创建 JsonDeserializer
// 2. User::deserialize(deserializer) 被调用
// - 创建 UserVisitor
// - deserializer.deserialize_map(UserVisitor)
// 3. Deserializer 解析 JSON,创建 JsonMapAccess
// 4. Deserializer 调用 visitor.visit_map(JsonMapAccess)
// 5. Visitor 调用 map.next_key::<String>() 和 map.next_value::<u32>()
// 6. Visitor 构造并返回 User
}
核心机制 - 控制反转:
| 组件 | 职责 | 不关心 |
|---|---|---|
| Deserialize | 知道如何构造自己 | 数据格式 |
| Visitor | 定义构造步骤 | 数据格式 |
| Deserializer | 解析格式,驱动 Visitor | 目标类型 |
| MapAccess | 抽象 map 访问接口 | 具体格式 |
优势:N 个数据类型 × M 个格式 = N + M 次实现(而非 N×M)
Serde 通过三层架构(Deserialize + Visitor + Deserializer)和数据访问层(MapAccess 等 trait)实现 Prism,让数据类型和格式解耦
本部分学习总结
核心收获
-
命令式 vs 声明式
- 命令式:描述how如何做(for 循环 + 状态变化)
- 声明式:描述what做什么(fold 函数组合)
- Rust 鼓励声明式:Iterator 链式调用
-
泛型作为类型类约束
- Java 泛型 = 类型擦除:运行时不存在泛型信息,用
Object替代 - C++ 和 Rust 泛型 = 单体化:编译时为每个类型生成独立代码
- 书中表述有误:C++ 和 Java 机制完全不同
- Rust 的
impl块语法比 C++ 模板特化更简洁 - 应用:Type State 模式,用泛型将运行时检查移到编译时
- Java 泛型 = 类型擦除:运行时不存在泛型信息,用
-
Functional Optics
- Iso:两固定类型转换
- Poly Iso:泛型转换
- Prism:支持多格式(Serde 的核心)
- Serde 三层架构:Deserialize + Visitor + Deserializer
- 数据访问层:
- 基础类型:直接传值(
visit_u64,visit_str) - 复杂类型:访问 trait(
MapAccess,SeqAccess,EnumAccess)
- 基础类型:直接传值(
- MapAccess 解耦 Visitor 和 Deserializer,支持按需解析
- 核心机制:Deserializer 反向调用 Visitor
实践指导
| 场景 | 推荐做法 |
|---|---|
| 编程思维 | 优先声明式(Iterator 链式调用) |
| 复杂对象构建 | 用泛型分离 API(Type State 模式) |
| 序列化/反序列化 | 使用 Serde,理解三层架构 |
| API 设计 | 用 trait 解耦,支持扩展 |
补充资源笔记 - Refactoring & Design Principles
来源:Rust Patterns - Additional Resources
原文版本:
main@f279f35(2026-03-29 查验)
📋 设计原则速查表
| # | 缩写 | 全称 | 一句话介绍 |
|---|---|---|---|
| 1 | SRP | Single Responsibility Principle | 一个模块只有一个引起它变化的原因 |
| 2 | OCP | Open/Closed Principle | 对扩展开放,对修改关闭 |
| 3 | LSP | Liskov Substitution Principle | Rust 中体现为实现同一 trait 的类型应遵守 trait 的语义约定 |
| 4 | ISP | Interface Segregation Principle | 多个小接口优于一个大接口 |
| 5 | DIP | Dependency Inversion Principle | 依赖抽象而非具体实现 |
| 6 | CRP | Composition Reuse Principle | 优先使用组合而非继承来复用代码 |
| 7 | DRY | Don’t Repeat Yourself | 每份知识只有一个表示 |
| 8 | KISS | Keep It Simple, Stupid | 简单应该是设计的关键目标 |
| 9 | LoD | Law of Demeter | 只和直接朋友交谈,不和朋友的朋友交谈 |
| 10 | DbC | Design by Contract | 用类型系统定义明确的接口规范 |
| 11 | - | Encapsulation | 隐藏内部实现,暴露受控接口 |
| 12 | CQS | Command-Query Separation | 查询无副作用,命令不返回值 |
| 13 | POLA | Principle of Least Astonishment | 行为应符合用户预期 |
| 14 | - | Uniform Access | 统一访问方式,不暴露存储还是计算 |
| 15 | - | Single-Choice | 选项列表只在一处定义 |
| 16 | - | Self-Documentation | 代码即文档,命名表达意图 |
| 17 | - | Linguistic-Modular-Units | 模块对应语言的语法单元 |
| 18 | - | Persistence-Closure | 存储对象时同时存储其依赖对象 |
Refactoring(重构)
核心要点
重构是将好代码变成优秀代码的关键过程。
关键原则
| 原则 | 说明 |
|---|---|
| 使用设计模式 | 用设计模式来 DRY 代码,泛化抽象 |
| 避免反模式 | 反模式虽然诱人,但弊大于利 |
| 使用 Idioms | 用习惯用法组织代码结构 |
重构的实践原则
| 原则 | 说明 |
|---|---|
| 先设计再重构 | 有整体视角和最终目标,避免盲目重构 |
| 无测试不重构 | 测试是重构的安全网 |
| 拆分环节 | 把整个重构过程拆分成可验证的小环节 |
| 多个检查点 | 每个环节完成后可以验证,确保正确 |
重构流程
1. 整体设计(目标视角)
↓
2. 拆分环节(可验证的小步骤)
↓
3. 设置检查点(测试、编译验证)
↓
4. 逐个小步重构(Small changes)
↓
5. 每个环节验证(Tests)
↓
6. 完成重构
关键名言
“Shortcuts make for long days.”(捷径导致长路)
SOLID 原则
SRP - Single Responsibility Principle(单一职责原则)
含义:一个模块应该只有一个引起它变化的原因。
Rust 体现:
- 小模块、小 crate
- 一个函数只做一件事
例子:
#![allow(unused)]
fn main() {
// ❌ 违反 SRP
fn process_user_data(user: &User) {
validate(user);
db.save(user);
send_email(&user.email);
log.info("user processed");
}
// ✅ 遵循 SRP
fn validate_user(user: &User) -> Result<()> { ... }
fn save_user(user: &User) -> Result<()> { ... }
fn send_welcome_email(email: &str) -> Result<()> { ... }
}
OCP - Open/Closed Principle(开闭原则)
含义:软件实体应该对扩展开放,对修改关闭。
Rust 体现:
- trait 扩展,无需修改现有代码
- 泛型允许编译时扩展
例子:
#![allow(unused)]
fn main() {
trait Shape {
fn area(&self) -> f64;
fn draw(&self);
}
// 添加新形状不需要修改现有代码
struct Triangle { base: f64, height: f64 }
impl Shape for Triangle {
fn area(&self) -> f64 { (self.base * self.height) / 2.0 }
fn draw(&self) { println!("Drawing triangle"); }
}
}
与 ISP 的关系:
- ISP 是 OCP 的前提:没有小接口,无法优雅扩展
- OCP 是 ISP 的目标:拆分接口是为了更容易扩展
LSP - Liskov Substitution Principle(里氏替换原则)
Rust 中的正确理解:
Rust 中没有传统 OOP 的里氏替换原则(因为没有类继承)。
Rust 中体现为:实现同一 trait 的类型应该在相关行为上遵守 trait 的语义约定。
例子:
#![allow(unused)]
fn main() {
// Eq trait 隐含的契约
// 实现 Eq 的类型必须满足等价关系的三个性质:
// 1. 自反性:a == a 总是 true
// 2. 对称性:a == b 等价于 b == a
// 3. 传递性:a == b 且 b == c 则 a == c
#[derive(Eq, PartialEq)]
struct UserId(u32); // ✅ 正确的实现
}
ISP - Interface Segregation Principle(接口隔离原则)
含义:多个专门的接口比一个通用的接口更好。
Rust 体现:
- 小 trait 优于大 trait
- 不应该强迫实现不需要的方法
例子:
#![allow(unused)]
fn main() {
// ❌ 违反 ISP
trait Worker {
fn work(&self);
fn eat(&self);
fn sleep(&self);
}
// ✅ 遵循 ISP
trait Workable { fn work(&self); }
trait Eatable { fn eat(&self); }
trait Sleepable { fn sleep(&self); }
}
DIP - Dependency Inversion Principle(依赖倒置原则)
含义:依赖抽象,而不是具体实现。
“倒置“的含义:
- 从“抽象依赖实现细节“ 倒置为 “实现细节依赖抽象,同时上层业务逻辑也依赖抽象”
Rust 体现:
- 依赖 trait 而非具体类型
dyn Trait或泛型参数
例子:
#![allow(unused)]
fn main() {
trait Database {
fn save_user(&self, user: &User) -> Result<()>;
}
struct UserService {
db: Box<dyn Database>, // 依赖抽象
}
}
其他设计原则
CRP - Composition Reuse Principle(组合复用原则)
别名:Composition over Inheritance(组合优于继承)
含义:优先使用对象组合,而不是类继承来复用代码。
Rust 体现:
- Rust 没有类继承,天然支持 CRP
- struct 组合 + trait
例子:
#![allow(unused)]
fn main() {
trait Engine {
fn start(&self);
fn drive(&self);
}
struct Car {
engine: Box<dyn Engine>, // 组合
door_count: u32,
}
// 可以动态改变行为
car.engine = Box::new(ElectricEngine);
}
DRY - Don’t Repeat Yourself(不要重复自己)
含义:系统中的每一份知识应该只有一个表示。
实践方法:
- 使用 CCD 工具(如 clippy)检测代码克隆
- 提取公共函数、公共类型
- 使用泛型、宏消除重复
Rust 工具:
| 工具 | 用途 |
|---|---|
| 函数 | 提取重复逻辑 |
| 泛型 | 提取重复的类型模式 |
| trait | 提取重复的行为 |
| 宏 | 提取重复的代码模式 |
例子:
#![allow(unused)]
fn main() {
// ✅ 用泛型消除重复
fn print_vec<T: std::fmt::Display>(v: &[T]) {
for item in v {
println!("{}", item);
}
}
// ✅ 用宏消除重复
macro_rules! impl_display {
($struct:ident, $unit:literal) => {
impl std::fmt::Display for $struct {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{} {}", self.0, $unit)
}
}
};
}
}
KISS - Keep It Simple, Stupid(保持简单)
含义:简单应该是设计的关键目标,应该避免不必要的复杂性。
与 DRY 的平衡:
- DRY 和 KISS 需要平衡
- 过度 DRY 会违反 KISS
- 有时适当重复比过度抽象更好
LoD - Law of Demeter(迪米特法则/最少知识原则)
含义:一个对象应该对其他对象有尽可能少的了解。
通俗理解:只和你的“直接朋友“交谈,不要和“朋友的朋友“交谈。
例子:
#![allow(unused)]
fn main() {
// ❌ 违反 LoD:链式调用
let city = user.profile.address.city;
// ✅ 遵循 LoD:委托访问
impl User {
fn get_city(&self) -> &str {
&self.profile.address.city
}
}
let city = user.get_city();
}
DbC - Design by Contract(契约设计)
含义:定义正式的接口规范,包括前置条件、后置条件和不变量。
核心:利用类型系统,通过设计良好的类型提供约束。
完整架构:
Design by Contract (Rust)
│
├── 类型系统(最强约束 - 编译时保证)
│ ├── 新类型模式 (Newtype)
│ ├── 类型状态模式 (Type State)
│ └── 生命周期约束
│
├── 返回类型(表达契约 - 编译时强制处理)
│ ├── Result<T, E> - 可能失败
│ ├── Option<T> - 可能为空
│ └── T - 保证成功
│
├── 运行时检查(辅助验证)
│ ├── assert!() - 总是检查
│ ├── debug_assert!() - 仅调试模式
│ └── panic!() - 违反契约
│
└── 测试(验证契约)
├── 单元测试 - 单个函数契约
├── 集成测试 - 模块间契约
├── 属性测试 (proptest) - 验证不变量
└── 文档测试 - API 示例验证
契约三要素:
| 要素 | 含义 | 责任方 |
|---|---|---|
| 前置条件 | 调用前必须满足的条件 | 调用者负责 |
| 后置条件 | 调用后保证的结果 | 实现者负责 |
| 不变量 | 始终成立的条件 | 双方共同维护 |
Encapsulation(封装)
含义:隐藏内部实现,暴露受控接口。
分层封装策略:
封装严格程度
│
├── 对外 API(最严格)
│ └── 私有字段 + 公共方法
│
├── 库内可见 pub(crate)(中等)
│ └── 模块间自由访问,对外隐藏
│
└── 模块内部(最宽松)
└── 非 pub struct + pub 字段(纯数据载体)
CQS - Command-Query Separation(命令查询分离)
含义:查询不应该有副作用,命令不应该返回值。
Rust 体现:
&self方法 = 查询(无副作用)&mut self方法 = 命令(有副作用)
例子:
#![allow(unused)]
fn main() {
impl Stack<T> {
// 查询
fn len(&self) -> usize { self.items.len() }
fn is_empty(&self) -> bool { self.items.is_empty() }
// 命令
fn push(&mut self, item: T) { self.items.push(item); }
fn pop(&mut self) -> Option<T> { self.items.pop() }
}
}
POLA - Principle of Least Astonishment(最少惊讶原则)
含义:组件的行为应该符合大多数用户的预期。
Rust 体现:
- 遵循命名约定(
new()创建新实例) - 运算符符合数学直觉
- 参考标准库的设计
Uniform Access(统一访问原则)
含义:所有服务应该通过统一的符号访问,不暴露是存储还是计算。
Rust 体现:
- 私有字段 + 公共 getter 方法
- 无参 getter 方法名与字段同名
例子:
#![allow(unused)]
fn main() {
struct Circle {
radius: f64, // 私有
}
impl Circle {
pub fn radius(&self) -> f64 { self.radius }
pub fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
// 调用者不关心哪些是字段,哪些是计算的
}
Single-Choice(单一选择原则)
含义:当系统需要支持一组替代方案时,应该只有一个模块知道它们的完整列表。
Rust 体现:
- enum 集中定义所有变体
- match 穷尽检查
例子:
#![allow(unused)]
fn main() {
pub enum PaymentMethod {
CreditCard,
PayPal,
BankTransfer,
// 添加新选项只需改这里
}
}
Self-Documentation(自文档化)
含义:代码即文档,命名表达意图。
实现方法:
- 清晰的命名
- 类型即文档
- 函数签名清晰
- 文档注释(rustdoc)
Linguistic-Modular-Units(语言模块单元)
含义:模块应该对应于所用语言的语法单元。
Rust 体现:
mod定义模块- 目录/文件对应模块
pub控制可见性
Persistence-Closure(持久化闭包)
含义:存储对象时同时存储其依赖对象,检索时也必须检索其依赖对象。
通俗理解:完整存储对象以及对象依赖的数据。
例子:
#![allow(unused)]
fn main() {
// ✅ 遵循:完整存储
struct User {
name: String,
address: Address, // 嵌套对象
}
// 序列化时自动包含 address
let json = serde_json::to_string(&user)?;
}
例外场景(何时不遵循):
| 场景 | 原因 | 替代方案 |
|---|---|---|
| 缓存行敏感 | 避免超过 64B 导致伪共享 | 用 ID/句柄引用冷数据 |
| 延迟加载需求 | 避免初始化慢、内存占用大 | 按需加载 |
| 循环依赖 | 无法完整存储 | 用 ID 打破循环 |
| 大数据集 | 无法一次性加载 | 流式处理 |
| 跨服务边界 | 数据所有权不清晰 | 用 ID 引用 |
| 共享数据 | 多个对象共享同一数据 | 独立存储 |
决策框架:
Persistence-Closure
│
├── 遵循:数据完整性优先
│ └── 小数据、强一致性、简单对象图
│
└── 不遵循:性能/灵活性优先
└── 缓存敏感、延迟加载、循环依赖、大数据
Additional Resources(额外资源)
Talks(演讲)
| 演讲 | 演讲者 | 场合 |
|---|---|---|
| Design Patterns in Rust | Nicholas Cameron | PDRust 2016 |
| Writing Idiomatic Libraries in Rust | Pascal Hertleif | RustFest 2017 |
| Rust Programming Techniques | Nicholas Cameron | LinuxConfAu 2018 |
Books (Online)
| 资源 | 链接 |
|---|---|
| The Rust API Guidelines | https://rust-lang.github.io/api-guidelines |
📝 学习总结
重构原则
- 先设计再重构(整体视角)
- 无测试不重构
- 拆分环节,设置检查点
- 小步迭代
SOLID 原则
| 原则 | Rust 体现 |
|---|---|
| SRP | 小模块、小 crate |
| OCP | trait 扩展 |
| LSP | trait 契约一致性 |
| ISP | 小 trait |
| DIP | 依赖 trait |
其他核心原则
| 原则 | 关键点 |
|---|---|
| CRP | Rust 无类继承,天然支持组合 |
| DRY | 用泛型/宏/trait 减少重复 |
| KISS | 简单优先,与 DRY 平衡 |
| LoD | 委托访问,避免链式调用 |
| DbC | 类型系统表达契约 |
| 封装 | 分层策略(对外严格,对内宽松) |
| CQS | &self vs &mut self |
| POLA | 符合预期,遵循约定 |
| Uniform Access | getter 方法统一访问 |
| Single-Choice | enum 集中定义选项 |
| Self-Documentation | 代码即文档 |
| Persistence-Closure | 完整存储对象图(有例外) |