类型状态模式
问题
如何用 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 块
- 泛型膨胀:多个状态维度组合导致类型爆炸
- 动态状态困难:运行时才知道的状态无法使用此模式(需要枚举)
适用于状态流程固定、需要强安全保证的场景。