PhantomData 与类型状态模式
问题
PhantomData 有什么用?如何用类型系统编码业务约束?
答案
PhantomData 基础
PhantomData<T> 是零大小类型(ZST),不占用内存,但告诉编译器"这个结构体逻辑上拥有 T":
use std::marker::PhantomData;
// 没有实际存储 T,但逻辑上与 T 相关
struct Id<T> {
value: u64,
_marker: PhantomData<T>,
}
struct User;
struct Order;
// 不同类型的 Id 不能混用
let user_id: Id<User> = Id { value: 1, _marker: PhantomData };
let order_id: Id<Order> = Id { value: 1, _marker: PhantomData };
// user_id == order_id // ❌ 编译错误:类型不同
PhantomData 的常见用途
1. 类型安全的标识符
struct UserId(PhantomData<User>, u64);
struct OrderId(PhantomData<Order>, u64);
fn get_user(id: UserId) -> User { /* ... */ }
// get_user(order_id) // ❌ 类型不匹配
2. 标记生命周期所有权
// 告诉编译器 Iter 逻辑上借用了 &'a T
struct Iter<'a, T> {
ptr: *const T,
end: *const T,
_marker: PhantomData<&'a T>, // 标记生命周期
}
3. 控制型变(variance)
use std::marker::PhantomData;
// PhantomData<T> → 协变于 T
// PhantomData<fn(T)> → 逆变于 T
// PhantomData<*mut T> → 不变于 T
struct Invariant<T> {
_marker: PhantomData<fn(T) -> T>, // 不变
}
类型状态模式(Typestate Pattern)
用类型系统在编译期强制执行状态转换规则:
// 定义状态(零大小类型)
struct Draft;
struct Review;
struct Published;
// 文章结构体,状态作为类型参数
struct Article<State> {
title: String,
content: String,
_state: PhantomData<State>,
}
// Draft 状态的方法
impl Article<Draft> {
fn new(title: String) -> Self {
Article {
title,
content: String::new(),
_state: PhantomData,
}
}
fn write(&mut self, text: &str) {
self.content.push_str(text);
}
// 提交审核:Draft → Review
fn submit(self) -> Article<Review> {
Article {
title: self.title,
content: self.content,
_state: PhantomData,
}
}
}
// Review 状态的方法
impl Article<Review> {
// 审核通过:Review → Published
fn approve(self) -> Article<Published> {
Article {
title: self.title,
content: self.content,
_state: PhantomData,
}
}
// 打回:Review → Draft
fn reject(self) -> Article<Draft> {
Article {
title: self.title,
content: self.content,
_state: PhantomData,
}
}
}
// Published 状态的方法
impl Article<Published> {
fn get_url(&self) -> String {
format!("/articles/{}", self.title)
}
}
fn main() {
let article = Article::<Draft>::new("Rust 类型状态".into());
// ✅ 合法的状态流转
let article = article.submit().approve();
println!("{}", article.get_url());
// ❌ 编译错误:Draft 没有 approve 方法
// Article::<Draft>::new("x".into()).approve();
// ❌ 编译错误:Published 没有 write 方法
// article.write("new content");
}
建造者模式 + 类型状态
struct NoUrl;
struct HasUrl(String);
struct NoMethod;
struct HasMethod(String);
struct RequestBuilder<U, M> {
url: U,
method: M,
headers: Vec<(String, String)>,
}
impl RequestBuilder<NoUrl, NoMethod> {
fn new() -> Self {
RequestBuilder { url: NoUrl, method: NoMethod, headers: vec![] }
}
}
impl<M> RequestBuilder<NoUrl, M> {
fn url(self, url: &str) -> RequestBuilder<HasUrl, M> {
RequestBuilder { url: HasUrl(url.to_string()), method: self.method, headers: self.headers }
}
}
impl<U> RequestBuilder<U, NoMethod> {
fn get(self) -> RequestBuilder<U, HasMethod> {
RequestBuilder { url: self.url, method: HasMethod("GET".into()), headers: self.headers }
}
}
// send() 只在 url 和 method 都设置后才可用
impl RequestBuilder<HasUrl, HasMethod> {
fn send(self) -> String {
format!("{} {}", self.method.0, self.url.0)
}
}
// RequestBuilder::new().send(); // ❌ 编译错误
// RequestBuilder::new().url("...").send(); // ❌ 编译错误
RequestBuilder::new().url("https://api.com").get().send(); // ✅
常见面试问题
Q1: PhantomData 占用多少内存?
答案:
零字节。PhantomData<T> 是零大小类型(ZST),不影响结构体的内存布局。它仅在编译期存在,用于类型检查、生命周期推断和型变控制。
Q2: 类型状态模式相比普通枚举状态有什么优势?
答案:
| 方案 | 错误发现时机 | 运行时检查 | API 清晰度 |
|---|---|---|---|
| 枚举状态 | 运行时 panic | 需要 | 所有方法都暴露 |
| 类型状态 | 编译时错误 | 不需要 | 只暴露当前状态的方法 |
枚举方案需要在每个方法里检查当前状态,不合法时 panic 或返回 Error。类型状态模式直接让非法操作无法编译。
Q3: PhantomData 如何影响 Drop 检查?
答案:
PhantomData<T> 告诉编译器"这个结构体逻辑上拥有 T",因此 Drop 检查器会认为 Drop 实现可能访问 T。这在使用裸指针的 unsafe 代码中很重要:
struct MyVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
_marker: PhantomData<T>, // 告诉编译器我们拥有 T
}
如果不加 PhantomData<T>,编译器不知道 MyVec 析构时会 drop T 的值,可能导致不正确的 drop 顺序。
Q4: 什么是零大小类型(ZST)?
答案:
ZST 是大小为 0 的类型,包括:
()单元类型PhantomData<T>- 空结构体
struct Empty; - 没有字段的枚举变体
ZST 不占用内存,Vec<()> 不分配堆内存。它们主要用于类型级编程和标记。