开场三问:Rust 新手别再被设计模式吓住

  1. 你手上有 Rust 项目,但除了 struct + impl,其他写法一概不敢碰?
  2. 你看过“设计模式”相关文章,却在 Rust 里找不到对口的代码范例?
  3. 你需要十个实打实的代码片段,能直接粘进 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 1tick 2tick 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(&copy));
}

输出两个计数值,说明数据仍在被多个所有者共享且安全受控。

失败复现与修复

  1. Newtype:把 fetch_user(ProductId(1001)) 注释取消,编译器会报错 expected struct 'UserId'——类型安全在编译期就卡住。
  2. Builder:删除 .host(...),运行时返回 missing host,提示你补齐必填项。
  3. Option/Result:把 parse_discount("30") 改成 parse_discount("thirty"),终端打印 discount error: invalid discount: thirty,引导你做输入校验。
  4. 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 悬空引用:迭代器里返回引用时要确保底层数据还活着,必要时用 ArcVec 持有数据。
  • RAII 误解:Drop 里只做清理,不要在 drop 实现里 panic!,否则会在 unwinding 中雪上加霜。

总结与下一步

  • 一句话复盘:掌握这 10 个设计模式,就能用 Rust 把“类型安全、资源安全、逻辑可插拔”串成一条顺滑路径。
  • 当下行动:把上面 10 段代码都跑一遍,再挑 2 个塞进你的真实项目里试水。
  • 持续打磨:接下来可以引入 serde 为 Builder 加序列化、用 tracing 给 Strategy 打日志、用 tokio 测试 Smart Pointer 在异步里的表现。

下一步行动清单

  1. 把这些 demo 函数化,汇总到一个 pattern_lab.rs,主程序通过命令行参数切换模式。
  2. 为 Builder + Option/Result 写一个简单 benchmark(cargo bench),比较不同构造方案的成本。
  3. 选一个真实业务场景,把 Strategy + Visitor + Smart Pointer 组合起来,实现插件式的扩展点。