名称: rust-设计模式 描述: “用于编写地道Rust代码的惯用法和设计模式。使用场景:(1)编写新的Rust代码,(2)审查Rust代码,(3)解决借用检查器问题,(4)设计Rust API。触发条件:Rust所有权问题、生命周期错误、API设计问题、‘如何在Rust中地道地实现X’。” 用户可调用: false
Rust 设计模式
编写地道Rust代码的惯用法和模式。重点关注利用所有权、借用和类型系统的Rust特有模式。
决策树
遇到问题?
├── 借用检查器错误?
│ ├── 需要从&mut枚举中移出值 → 使用mem::take/replace
│ ├── 需要独立的字段借用 → 结构体解构
│ ├── 想用克隆? → 检查:Rc/Arc?或重构所有权?
│ └── 生命周期太短 → 考虑使用拥有所有权的类型或'static
│
├── API设计问题?
│ ├── 多个构造参数 → 建造者模式
│ ├── 接受灵活输入 → 借用类型(&str, &[T])
│ ├── 编译时类型安全 → 新类型模式
│ ├── 需要默认值 → Default trait + 结构体更新语法
│ └── 需要资源清理 → RAII守卫(Drop trait)
│
├── FFI边界问题?
│ ├── 错误处理 → 整数错误码 + 错误描述函数
│ ├── 字符串传递 → CString/CStr模式
│ └── 对象生命周期 → 不透明指针配合显式释放
│
├── 不安全代码?
│ ├── 需要不安全操作 → 封装在小模块中并提供安全包装
│ └── FFI类型 → 类型整合到不透明包装器中
│
└── 性能问题?
├── 避免单态化膨胀 → 栈上动态分发
└── 减少内存分配 → 使用mem::take替代clone
快速模式
借用类型(关键)
优先使用&str而非&String,&[T]而非&Vec<T>:
// 不好:只接受&String
fn process(s: &String) { }
// 好:接受&String, &str, 字符串字面量
fn process(s: &str) { }
// 用法:所有类型都适用于&str
process(&my_string); // String
process("literal"); // &'static str
process(&my_string[1..5]); // 切片
原因:Deref强制转换允许&String → &str,但反过来不行。使用借用类型可以接受更多输入类型。
mem::take模式(关键)
从&mut中移出拥有所有权的值而无需克隆:
use std::mem;
enum State {
Active { data: String },
Inactive,
}
fn deactivate(state: &mut State) {
if let State::Active { data } = state {
// 无需克隆即可获得所有权
let owned_data = mem::take(data);
*state = State::Inactive;
// 使用owned_data...
}
}
适用场景:更改枚举变体同时保留内部拥有所有权的数据。避免克隆反模式。
新类型模式
零运行时成本的类型安全:
// 不同的类型防止混淆
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) { }
// 编译错误:不能混淆ID
// get_order(order_id, user_id);
适用场景:需要在相同底层类型之间进行编译时区分,或需要自定义trait实现。
建造者模式
用于复杂构造:
#[derive(Default)]
struct RequestBuilder {
url: String,
timeout: Option<u32>,
headers: Vec<(String, String)>,
}
impl RequestBuilder {
fn url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
fn timeout(mut self, ms: u32) -> Self {
self.timeout = Some(ms);
self
}
fn build(self) -> Request {
Request { /* ... */ }
}
}
// 用法
let req = RequestBuilder::default()
.url("https://example.com")
.timeout(5000)
.build();
适用场景:多个可选参数,或构造过程需要验证/副作用。
RAII守卫
通过所有权进行资源管理:
struct FileGuard {
file: File,
}
impl Drop for FileGuard {
fn drop(&mut self) {
// 守卫超出作用域时自动运行清理
self.file.sync_all().ok();
}
}
fn process() -> Result<()> {
let guard = FileGuard { file: File::open("data.txt")? };
// 即使提前返回或发生panic,Drop也会运行
do_work()?;
Ok(())
} // guard.drop()在此处调用
适用场景:需要保证清理(锁、文件、连接、事务)。
Default + 结构体更新
使用默认值进行部分初始化:
#[derive(Default)]
struct Config {
host: String,
port: u16,
timeout: u32,
retries: u8,
}
let config = Config {
host: "localhost".into(),
port: 8080,
..Default::default() // timeout=0, retries=0
};
栈上动态分发
避免为trait对象进行堆分配:
use std::io::{self, Read};
fn process(use_stdin: bool) -> io::Result<String> {
let readable: &mut dyn Read = if use_stdin {
&mut io::stdin()
} else {
&mut std::fs::File::open("input.txt")?
};
let mut buf = String::new();
readable.read_to_string(&mut buf)?;
Ok(buf)
}
适用场景:需要动态分发但不想使用Box分配。自Rust 1.79起,生命周期扩展使此用法更符合人体工程学。
Option作为迭代器
Option实现了IntoIterator(0或1个元素):
let maybe_name = Some("Turing");
let mut names = vec!["Curry", "Kleene"];
// 使用Option扩展
names.extend(maybe_name);
// 与Option链式调用
for name in names.iter().chain(maybe_name.iter()) {
println!("{name}");
}
提示:对于总是Some的情况,优先使用std::iter::once(value)。
闭包捕获控制
通过重新绑定控制闭包捕获的内容:
use std::rc::Rc;
let num1 = Rc::new(1);
let num2 = Rc::new(2);
let closure = {
let num2 = num2.clone(); // 移动前克隆
let num1 = num1.as_ref(); // 借用
move || { *num1 + *num2 }
};
// num1仍可用,num2已被克隆
临时可变性
设置完成后使变量不可变:
// 方法1:嵌套块
let data = {
let mut data = get_vec();
data.sort();
data
};
// 此处data不可变
// 方法2:重新绑定
let mut data = get_vec();
data.sort();
let data = data; // 现在不可变
错误时返回消耗的参数
如果函数消耗了参数,在错误时返回它以进行重试:
pub struct SendError(pub String); // 包含原始值
pub fn send(value: String) -> Result<(), SendError> {
if can_send() {
do_send(&value);
Ok(())
} else {
Err(SendError(value)) // 调用者可以重试
}
}
// 用法:无需克隆的重试循环
let mut msg = "hello".to_string();
loop {
match send(msg) {
Ok(()) => break,
Err(SendError(m)) => { msg = m; } // 恢复并重试
}
}
示例:String::from_utf8返回包含原始Vec<u8>的FromUtf8Error。
反模式检查清单
检查代码中这些常见错误:
-
[ ] 为满足借用检查器而克隆 - 通常表示所有权设计问题。考虑使用
mem::take、Rc/Arc或重构。 -
[ ] 在库中使用
#![deny(warnings)]- 在新Rust版本中会破坏下游代码。在CI中使用RUSTFLAGS="-D warnings"代替。 -
[ ] 使用Deref实现继承 - 行为令人意外,不提供真正的子类型化。使用组合+委托或trait。
-
[ ] 函数参数使用
&String或&Vec<T>- 使用&str或&[T]以获得灵活性。 -
[ ] 手动调用
drop()- 通常不必要。如果需要控制顺序,优先使用作用域块。 -
[ ] 忽略clippy对
.clone()的建议 - 运行cargo clippy查找不必要的克隆。
参考资料
包含完整示例的详细模式:
- ownership-patterns.md - 借用检查器模式:mem::take/replace、结构体解构、RAII守卫、Rc/Arc决策
- api-design.md - API模式:借用类型、建造者、新类型、Default trait、FFI
- common-pitfalls.md - 详细反模式:克隆滥用、deny(warnings)、Deref多态