跳到主要内容

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<()> 不分配堆内存。它们主要用于类型级编程和标记。

相关链接