跳到主要内容

类型状态模式

问题

如何用 Rust 的类型系统在编译时保证状态转换的正确性?

答案

类型状态模式利用泛型标记将状态编码到类型中,非法的状态转换在编译时就会被拒绝。

关于 PhantomData 的基础用法请参考 PhantomData

典型示例:HTTP 请求构建器

use std::marker::PhantomData;

// 状态标记(零大小类型)
struct Draft;
struct Ready;
struct Sent;

struct HttpRequest<S> {
url: String,
method: String,
body: Option<String>,
_state: PhantomData<S>,
}

impl HttpRequest<Draft> {
fn new(url: &str) -> Self {
HttpRequest {
url: url.to_string(),
method: "GET".to_string(),
body: None,
_state: PhantomData,
}
}

fn method(mut self, m: &str) -> Self {
self.method = m.to_string();
self
}

fn body(mut self, b: &str) -> Self {
self.body = Some(b.to_string());
self
}

// Draft → Ready
fn prepare(self) -> HttpRequest<Ready> {
HttpRequest {
url: self.url,
method: self.method,
body: self.body,
_state: PhantomData,
}
}
}

impl HttpRequest<Ready> {
// Ready → Sent
async fn send(self) -> HttpRequest<Sent> {
println!("{} {} {:?}", self.method, self.url, self.body);
HttpRequest {
url: self.url,
method: self.method,
body: self.body,
_state: PhantomData,
}
}
}

impl HttpRequest<Sent> {
fn status(&self) -> u16 { 200 }
}

// 使用
let req = HttpRequest::new("https://api.example.com")
.method("POST")
.body(r#"{"name":"Alice"}"#)
.prepare() // Draft → Ready
.send() // Ready → Sent
.await;

println!("Status: {}", req.status());

// req.send().await; // ❌ 编译错误:Sent 没有 send 方法
// HttpRequest::new("...").send(); // ❌ 编译错误:Draft 没有 send 方法

实际应用

场景状态编译时保证
TCP 连接未连接 → 已连接 → 已关闭不能在未连接时发送
文件操作已打开 → 已写入 → 已关闭不能写入已关闭的文件
事务开始 → 操作中 → 提交/回滚不能重复提交

常见面试问题

Q1: 类型状态模式有什么缺点?

答案

  • 代码量增加:每个状态转换需要独立的 impl 块
  • 泛型膨胀:多个状态维度组合导致类型爆炸
  • 动态状态困难:运行时才知道的状态无法使用此模式(需要枚举)

适用于状态流程固定、需要强安全保证的场景。

相关链接