Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Idioms 学习笔记

来源:Rust Unofficial Patterns

原文版本: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 推荐)
  • 参数多 → 使用 bon crate 的 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)✅ 自动解引用
StringDeref<Target=str>&str✅ 自动解引用
HashMap<K,V>❌ 仅普通借用
Box<T>Deref<Target=T>&T✅ 智能指针

智能指针集合Vec, String 实现 Deref,自动解引用到借用视图;非智能指针集合HashMap, BTreeMap 等没有 DerefAPI 设计:方法通常实现在借用视图上


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,留下 Nonereplace(&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()
};
}

详见 2.4 The Default Trait

..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)  // 返回原字符串
}
}

错误时返回原始输入,调用者可重试或记录,而非直接丢弃


本部分学习总结

核心收获

  1. 借用优先 - 参数用 &T,避免不必要的所有权转移
  2. format! 宏 - 字符串拼接首选
  3. Default trait - 无参构造的最佳实践
  4. Move 语义 - 集合类型赋值会转移所有权
  5. RAII - 同步资源用 Drop,async 资源用显式 close
  6. mem 工具 - replace/take 用于所有权交换
  7. 栈上分发 - 临时批量处理避免堆分配

最佳实践

场景推荐做法
函数参数&TAsRef<T>
字符串format! / indoc
构造new() / Default / bon
资源清理RAII / 显式 close()
集合传参&Vec / &HashMap

常见陷阱

  • ❌ 忘记集合是 Move 语义
  • ❌ async 资源在 Drop 中无法清理
  • ❌ 返回栈上的 trait object 引用

Rust Design Patterns 学习笔记

来源:Rust Unofficial Patterns

原文版本:main@f279f35 (2026-03-29 查验)

部分:Part 2 - Design Patterns (设计模式)


概述

设计模式是解决常见编程问题的经典方法。本章分为 4 个类别:

  • Behavioural (行为型): 对象间的通信和职责分配
  • Creational (创建型): 对象的创建机制
  • Structural (结构型): 如何组合类和结构体
  • FFI: 与其他语言交互的模式

共 14 个小节


目录

Behavioural Patterns

Creational Patterns

Structural Patterns

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.0 vs url:0.5 类型不兼容)
  • 缺乏审核:crates.io 不审核,可能质量差或恶意
  • 性能损失:默认无 LTO,优化差

用户实践原则:如无必要则不拆,要拆则精心设计

  1. 不主动拆分

    • 避免过度工程化
    • 减少依赖管理复杂度
  2. 需要拆分时

    • 做好完善的设计(清晰的 API 边界)
    • 完善的测试(减少版本冲突风险)
    • 考虑语义化版本(semver)兼容性
  3. 拆分时机

    • 功能真正独立且可复用
    • 有明确的责任边界
    • 可能被其他项目使用

示例:

// 好的拆分:
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)而产生的模式
  • 要点
    1. unsafe 代码集中起来
    2. 集中检查和提供安全保证
    3. 明确安全和不安全的边界
    4. 同时通过 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“模式,明确所有权和生命周期边界。

设计原则:

类型所有权管理可见性
EncapsulatedRust 拥有用户管理不透明
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”

本部分学习总结

核心收获

  1. 类型安全思维(Newtype)

    • 用类型系统把隐式假设变成显式保证
    • 零开销抽象
  2. 资源管理思维(RAII)

    • 同步资源用 Drop
    • async 资源用显式 close
  3. 安全边界思维(Contain unsafety + FFI 模式)

    • unsafe 集中到最小模块,对外提供安全接口
    • FFI 中数据在 Rust 内部,unsafe 只在边界
    • 把“持有引用的迭代器“变成“带状态索引的包装器“

模式质量评价

真正重要的模式

  • Newtype、RAII Guards、Contain unsafety —— 充分利用 Rust 安全特性

特定场景有用

  • Visitor、Strategy、Builder —— 经典设计模式的 Rust 实现

有争议的模式

  • Compose Structs —— 本质是借用检查基本技巧,不是设计模式
  • 部分示例为了模式而模式,没有清晰对比优劣

Idioms vs Patterns 区别

方面IdiomsPatterns
定位日常编码习惯设计模板
Rust 特色借用、所有权类型系统、安全边界
使用频率中/低

实践指导

场景推荐模式
防止类型混淆Newtype
资源清理RAII / 显式 close
包装 unsafeContain unsafety
复杂对象构建Builder(bon crate)
AST 处理Visitor / Fold
FFI 库设计Object-Based APIs + Type Consolidation

Rust Anti-Patterns 学习笔记

来源:Rust Unofficial 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 的稳定性保证。

问题:

  1. 破坏 Rust 的稳定性保证

    • Rust 有时会用新的 lint 警告某些问题
    • 经过一段时间后才变成 deny(硬错误)
    • #![deny(warnings)]立即把这些警告变成错误
  2. 无法灵活处理警告

    • API 废弃会发出警告
    • 新版本引入新 lint 时可能编译失败
  3. 无法使用 clippy 等工具

替代方案:

  1. 命令行设置(推荐):

    RUSTFLAGS="-D warnings" cargo build
    
  2. 明确指定要 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>T
  • Rc<T>T
  • Stringstr
  • 自定义智能指针

替代方案

  1. 手动转发(推荐):

    #![allow(unused)]
    fn main() {
    impl Bar {
        fn m(&self) {
            self.f.m()  // 清晰明确
        }
    }
    }
  2. 使用 trait 抽象

  3. 使用委托 crate

不要用 Deref 模拟继承,应该手动转发或使用 trait/委托 crate


本部分学习总结

核心收获

  1. 借用优先于 clone

    • clone 应该是有意为之的数据拷贝决策
    • 能借用时尽量借用
  2. 警告管理的正确方式

    • 不用 #![deny(warnings)] blanket 禁止
    • 用命令行或明确列出具体 lint
  3. Deref 的正确用途

    • 只用于智能指针解引用
    • 不用来模拟继承

实践指导

场景推荐做法
借用检查报错先想借用,再考虑 clone
CI 严格要求RUSTFLAGS="-D warnings"
方法复用手动转发 / trait / 委托宏
智能指针正确使用 Deref

Rust Functional Programming 学习笔记

来源:Rust Unofficial Patterns

原文版本: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>两个不同的类型
  • 编译器会为每个具体类型生成独立代码
  • 优点:运行时无开销;缺点:代码膨胀

三种语言泛型机制对比

特性JavaC++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 静态验证引脚配置
HTTPhyper 不同连接器有不同方法
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 绑定具体 DeserializerVisitor 只依赖 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>;
}
}

类型约束关系

类型参数级别绑定到决定方
'detrait 级MapAccess<'de>Deserializer
type Error关联类型MapAccessDeserializer
V (visit_map)方法级visit_mapDeserializer
K (next_key)方法级next_keyVisitor
V (next_value)方法级next_valueVisitor

具体实现

格式库V 的具体类型
serde_jsonJsonMapAccess<'de>
serde_yamlYamlMapAccess<'de>
serde_cborCborMapAccess<'de>
serde_xmlXmlMapAccess<'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,让数据类型和格式解耦


本部分学习总结

核心收获

  1. 命令式 vs 声明式

    • 命令式:描述how如何做(for 循环 + 状态变化)
    • 声明式:描述what做什么(fold 函数组合)
    • Rust 鼓励声明式:Iterator 链式调用
  2. 泛型作为类型类约束

    • Java 泛型 = 类型擦除:运行时不存在泛型信息,用 Object 替代
    • C++ 和 Rust 泛型 = 单体化:编译时为每个类型生成独立代码
    • 书中表述有误:C++ 和 Java 机制完全不同
    • Rust 的 impl 块语法比 C++ 模板特化更简洁
    • 应用:Type State 模式,用泛型将运行时检查移到编译时
  3. 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 查验)


📋 设计原则速查表

#缩写全称一句话介绍
1SRPSingle Responsibility Principle一个模块只有一个引起它变化的原因
2OCPOpen/Closed Principle对扩展开放,对修改关闭
3LSPLiskov Substitution PrincipleRust 中体现为实现同一 trait 的类型应遵守 trait 的语义约定
4ISPInterface Segregation Principle多个小接口优于一个大接口
5DIPDependency Inversion Principle依赖抽象而非具体实现
6CRPComposition Reuse Principle优先使用组合而非继承来复用代码
7DRYDon’t Repeat Yourself每份知识只有一个表示
8KISSKeep It Simple, Stupid简单应该是设计的关键目标
9LoDLaw of Demeter只和直接朋友交谈,不和朋友的朋友交谈
10DbCDesign by Contract用类型系统定义明确的接口规范
11-Encapsulation隐藏内部实现,暴露受控接口
12CQSCommand-Query Separation查询无副作用,命令不返回值
13POLAPrinciple 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 RustNicholas CameronPDRust 2016
Writing Idiomatic Libraries in RustPascal HertleifRustFest 2017
Rust Programming TechniquesNicholas CameronLinuxConfAu 2018

Books (Online)

资源链接
The Rust API Guidelineshttps://rust-lang.github.io/api-guidelines

📝 学习总结

重构原则

  • 先设计再重构(整体视角)
  • 无测试不重构
  • 拆分环节,设置检查点
  • 小步迭代

SOLID 原则

原则Rust 体现
SRP小模块、小 crate
OCPtrait 扩展
LSPtrait 契约一致性
ISP小 trait
DIP依赖 trait

其他核心原则

原则关键点
CRPRust 无类继承,天然支持组合
DRY用泛型/宏/trait 减少重复
KISS简单优先,与 DRY 平衡
LoD委托访问,避免链式调用
DbC类型系统表达契约
封装分层策略(对外严格,对内宽松)
CQS&self vs &mut self
POLA符合预期,遵循约定
Uniform Accessgetter 方法统一访问
Single-Choiceenum 集中定义选项
Self-Documentation代码即文档
Persistence-Closure完整存储对象图(有例外)