开场三问:Rust 新手别再被设计模式吓住
- 你手上有 Rust 项目,但除了
struct + impl,其他写法一概不敢碰? - 你看过“设计模式”相关文章,却在 Rust 里找不到对口的代码范例?
- 你需要十个实打实的代码片段,能直接粘进
main.rs就跑?这篇一次给全。
快速类比:像装修新房一样挑选工具
写 Rust 就像装修自家新房:Newtype 是门禁卡,防止陌生人乱入;Builder 是施工清单,确保螺丝不漏;Option/Result 是验收表,缺失就返工;Trait 策略像可替换的工头;Visitor 是巡检;智能指针是共享工具柜。按节奏用工具,房子就稳稳地交付。
原理速写:10 个模式怎么串成 workflow
- 类型安全护栏:Newtype + Option/Result 在编译期提醒你数据是不是好使。
- 构造流程排程:Builder 让复杂结构一次性装配好,State + Iterator 把流程拆成小步。
- 行为随时可插拔:Strategy、Extension Trait、Visitor 都是“先约定接口,再替换实现”的套路。
- 资源自动收尾:RAII 保证出作用域就清理;Smart Pointer 负责托管共享数据,防止多线程撕扯。
- 组合大于拼凑:这 10 个模式配合使用,能撑起一个安全、可维护、易扩展的 Rust 项目骨架。
实战步骤:10 个模式逐个练
先创建一个练习仓库,后面每段代码都可以单独粘进去跑:
rustup override set stable
rustup component add rustfmt clippy
cargo new rust-pattern-playbook --bin
cd rust-pattern-playbook
提示:下面的 10 段代码都是独立 demo,要测试哪一段,就把当前
src/main.rs的内容替换成对应代码,再cargo run。
1. Newtype:给原始类型戴上门禁卡
把“用户 ID”“商品 ID”分开,编译器就能帮你挡住乌龙调用。
#[derive(Debug)]
struct UserId(u64);
#[derive(Debug)]
struct ProductId(u64);
fn fetch_user(id: UserId) {
println!("fetching user {:?}", id);
}
fn main() {
let user_id = UserId(1001);
fetch_user(user_id);
// fetch_user(ProductId(1001)); // 取消注释试试,编译器直接拦住
}
跑完你能看到 fetching user UserId(1001),Newtype 直接把类型安全兜住。
2. Builder:复杂结构的施工清单
把可选项和必填项拆开,构建大型结构时不再眼花缭乱。
#[derive(Debug)]
struct Server {
host: String,
port: u16,
timeout: u64,
}
struct ServerBuilder {
host: Option<String>,
port: u16,
timeout: u64,
}
impl ServerBuilder {
fn new() -> Self {
Self {
host: None,
port: 8080,
timeout: 30,
}
}
fn host(mut self, host: &str) -> Self {
self.host = Some(host.to_string());
self
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
fn build(self) -> Result<Server, String> {
Ok(Server {
host: self.host.ok_or("missing host")?,
port: self.port,
timeout: self.timeout,
})
}
}
fn main() -> Result<(), String> {
let server = ServerBuilder::new()
.host("0.0.0.0")
.port(3000)
.timeout(60)
.build()?;
println!("server config: {:?}", server);
Ok(())
}
注释掉 .host("0.0.0.0") 再跑,Builder 会立刻抛错 missing host。
3. Option & Result:缺席和错误都要有台阶
没有 null,就用 Option 表示“可能缺”;Result 表示“可能错”。
#[derive(Debug)]
struct User {
id: u32,
name: String,
}
fn find_user(id: u32) -> Option<User> {
if id == 7 {
Some(User { id, name: "Alice".into() })
} else {
None
}
}
fn parse_discount(input: &str) -> Result<u8, String> {
input.parse::<u8>().map_err(|_| format!("invalid discount: {}", input))
}
fn main() {
match find_user(7) {
Some(user) => println!("found user: {:?}", user),
None => println!("user not found"),
}
match parse_discount("30") {
Ok(rate) => println!("apply {}% off", rate),
Err(err) => println!("discount error: {}", err),
}
}
把 find_user(7) 改成 find_user(8),或者 parse_discount("thirty"),错误路径立刻显形。
4. Strategy:不同算法同一个接口
先约好 trait,运行时再挑“哪套策略”上场。
trait PricingStrategy {
fn quote(&self, seats: u32) -> f64;
}
struct FullPrice;
struct Tiered;
impl PricingStrategy for FullPrice {
fn quote(&self, seats: u32) -> f64 {
seats as f64 * 99.0
}
}
impl PricingStrategy for Tiered {
fn quote(&self, seats: u32) -> f64 {
let base = seats as f64 * 99.0;
if seats > 10 { base * 0.8 } else { base }
}
}
fn checkout(strategy: &dyn PricingStrategy, seats: u32) {
println!("strategy price: {:.2}", strategy.quote(seats));
}
fn main() {
checkout(&FullPrice, 5);
checkout(&Tiered, 12);
}
输出一行原价、一行折扣价;换策略只用换引用,不用改主流程。
5. RAII:资源出作用域自己收尾
把资源交给作用域,离开时自动清理。
use std::fs::File;
use std::io::{self, Write};
fn write_log(path: &str, message: &str) -> io::Result<()> {
let mut file = File::create(path)?;
file.write_all(message.as_bytes())?;
println!("log written, file will auto-close");
Ok(())
}
fn main() -> io::Result<()> {
write_log("demo.log", "user signed in")?;
// 文件离开作用域后自动关闭,无需手动 drop
Ok(())
}
运行后你能在目录里看到 demo.log,同时不担心文件句柄泄漏。
6. State:把流程拆成有限状态机
用 enum 表示状态,转换路径一目了然。
#[derive(Debug)]
enum SyncState {
Idle,
Connecting,
Connected { session: String },
Failed { error: String },
}
struct SyncClient {
state: SyncState,
}
impl SyncClient {
fn new() -> Self {
Self { state: SyncState::Idle }
}
fn connect(&mut self) {
self.state = SyncState::Connecting;
}
fn complete(&mut self, session: &str) {
self.state = SyncState::Connected { session: session.into() };
}
fn fail(&mut self, error: &str) {
self.state = SyncState::Failed { error: error.into() };
}
}
fn main() {
let mut client = SyncClient::new();
println!("state: {:?}", client.state);
client.connect();
println!("state: {:?}", client.state);
client.complete("abc-123");
println!("state: {:?}", client.state);
}
输出会按顺序展示 Idle → Connecting → Connected,失败分支同理。
7. Iterator:自定义迭代器走小步快跑
实现 Iterator trait,就能把自定义结构塞进 for 循环。
struct Counter {
current: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Self { current: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
fn main() {
for value in Counter::new(3) {
println!("tick {}", value);
}
}
cargo run 会打印 tick 1、tick 2、tick 3,零额外成本。
8. Extension Trait:给外部类型加私房技能
不能修改标准库,就用 trait 扩展方法。
trait StringExtras {
fn to_snippet(&self, len: usize) -> String;
}
impl StringExtras for String {
fn to_snippet(&self, len: usize) -> String {
if self.len() > len {
format!("{}...", &self[..len])
} else {
self.clone()
}
}
}
fn main() {
let slogan = "This is a very long sentence".to_string();
println!("snippet: {}", slogan.to_snippet(10));
}
输出 snippet: This is a ...,常用于日志、列表页截断。
9. Visitor:让巡检逻辑跟数据结构解耦
一份接口处理多种数据,让“检查”逻辑集中管理。
trait Visitor {
fn visit_number(&self, value: i32);
fn visit_text(&self, value: &str);
}
struct PrintVisitor;
impl Visitor for PrintVisitor {
fn visit_number(&self, value: i32) {
println!("number: {}", value);
}
fn visit_text(&self, value: &str) {
println!("text: {}", value);
}
}
enum Data {
Number(i32),
Text(String),
}
trait Visitable {
fn accept(&self, visitor: &dyn Visitor);
}
impl Visitable for Data {
fn accept(&self, visitor: &dyn Visitor) {
match self {
Data::Number(v) => visitor.visit_number(*v),
Data::Text(t) => visitor.visit_text(t),
}
}
}
fn main() {
let items = vec![Data::Number(10), Data::Text("hello".into())];
let visitor = PrintVisitor;
for item in &items {
item.accept(&visitor);
}
}
运行后分别打印数字和文本,后续新增数据类型时只改枚举,不改访客。
10. Smart Pointer:共享数据但不撕扯所有权
Rc 适合单线程共享,Arc 适合多线程;引用计数帮你看清谁还在用。
use std::rc::Rc;
use std::sync::Arc;
fn main() {
let rc_tags = Rc::new(vec!["rust", "design-patterns"]);
let another = Rc::clone(&rc_tags);
println!("rc strong count: {}", Rc::strong_count(&another));
let arc_label = Arc::new(String::from("shared-config"));
let copy = Arc::clone(&arc_label);
println!("arc strong count: {}", Arc::strong_count(©));
}
输出两个计数值,说明数据仍在被多个所有者共享且安全受控。
失败复现与修复
- Newtype:把
fetch_user(ProductId(1001))注释取消,编译器会报错expected struct 'UserId'——类型安全在编译期就卡住。 - Builder:删除
.host(...),运行时返回missing host,提示你补齐必填项。 - Option/Result:把
parse_discount("30")改成parse_discount("thirty"),终端打印discount error: invalid discount: thirty,引导你做输入校验。 - RAII:在
write_log中调用drop(file);再写入,会得到编译错误“借用已被移动”,提醒你资源生命周期必须按作用域来。
性能与权衡
- Newtype/Option/Result 带来零运行时开销,却换来编译期校验;千万别为了省字符用裸类型。
- Builder 多了一次对象构建,但配置清晰度暴增;性能敏感区可以提供
with_*快速通道。 - Strategy/Visitor 使用 trait object 时有一次动态分发;在热路径可改用枚举 +
match,但失去运行时扩展性。 - Iterator 编译器会内联
next,配合for基本没有额外成本。 - Smart Pointer 的引用计数和锁(Arc + Mutex)有开销,但换来安全共享;并发代码可以考虑
DashMap或分段锁优化争用。
常见坑与对策
- 生命周期不足:Visitor 场景传引用时,要么复制成
String,要么用Arc<str>;别把短生命周期借用塞进长生命周期容器。 - Rc/Arc 混用:跨线程一定用
Arc,别把Rc偷进多线程环境;需要可变访问时考虑Arc<Mutex<T>>或Arc<RwLock<T>>。 - Builder 数据遗失:
build()后别忘了处理Result;一旦.expect(),失败时就失去友好的错误提示。 - Iterator 悬空引用:迭代器里返回引用时要确保底层数据还活着,必要时用
Arc或Vec持有数据。 - RAII 误解:Drop 里只做清理,不要在
drop实现里panic!,否则会在 unwinding 中雪上加霜。
总结与下一步
- 一句话复盘:掌握这 10 个设计模式,就能用 Rust 把“类型安全、资源安全、逻辑可插拔”串成一条顺滑路径。
- 当下行动:把上面 10 段代码都跑一遍,再挑 2 个塞进你的真实项目里试水。
- 持续打磨:接下来可以引入
serde为 Builder 加序列化、用tracing给 Strategy 打日志、用tokio测试 Smart Pointer 在异步里的表现。
下一步行动清单
- 把这些 demo 函数化,汇总到一个
pattern_lab.rs,主程序通过命令行参数切换模式。 - 为 Builder + Option/Result 写一个简单 benchmark(
cargo bench),比较不同构造方案的成本。 - 选一个真实业务场景,把 Strategy + Visitor + Smart Pointer 组合起来,实现插件式的扩展点。