在写底层 Rust(尤其是 unsafe / 裸指针 / FFI)时,你会遇到一种常见矛盾:
- 运行时:你手里可能只有一个
*const T/*mut T/*mut c_void(比如外部库返回的句柄),结构体里并没有真正存放某个引用或某个类型的值。 - 编译期:你又希望编译器知道“我这个类型和某个生命周期/类型绑定”,从而帮你做借用检查、推导
Send/Sync、避免错误混用等。
std::marker::PhantomData<T> 就是为了解决这个问题而存在的工具。官方文档的核心定义是:
PhantomData<T>是一个 零大小类型(ZST),用于标记你的类型“行为上像是拥有/包含 一个T”,尽管你实际上并没有存储T。这会影响编译器计算一些安全相关属性。
也因此,它常被称为“只在编译期生效的零成本抽象”。
size_of::<PhantomData<T>>() == 0align_of::<PhantomData<T>>() == 1
下面用两个最典型的场景来科普:
- 绑定生命周期(防悬垂)
- 绑定类型参数(防混用)
1. 绑定生命周期:裸指针不带生命周期,需要用 PhantomData<&'a T> 把借用关系“说清楚”
假设你想实现一个 slice/数组的迭代器或视图,内部用裸指针表示范围:
1struct Slice<T> { 2 start: *const T, 3 end: *const T, 4} 5
如果这些指针来自某个外部数据(比如 Vec<T> / &[T]),为了避免悬垂指针,你的真实意图是:
Slice不能活得比原始数据更久
但问题是:*const T 不携带生命周期,编译器无法从字段中推导“它借用了谁、借用了多久”。于是你会想加生命周期参数:
1struct Slice<'a, T> { 2 start: *const T, 3 end: *const T, 4} 5
这会立刻遇到编译器抱怨:'a 没有被使用(unused lifetime parameter)。更重要的是:即使你强行让它通过,编译器也仍然不知道 'a 和哪些数据有关。
正确写法:加一个“假装持有引用”的标记字段
1use std::marker::PhantomData; 2 3struct Slice<'a, T> { 4 start: *const T, 5 end: *const T, 6 _marker: PhantomData<&'a T>, 7} 8
PhantomData<&'a T> 的含义可以直译为:
“请把我当成好像内部存了一个
&'a T引用。”
于是类型系统就会把 Slice<'a, T> 当成“借用了 'a 的 T”,从而强制它不能活过 'a。
坏例子:没有生命周期绑定,能编译,但可能产生悬垂指针(UB)
下面这段代码演示了“裸指针 + 没有 'a”的危险:它能把指向局部 Vec 的指针带出函数。
注意:这段代码可能触发未定义行为(UB) ,请不要在真实项目里这么写。
1struct SliceIterBad<T> { 2 ptr: *const T, 3 len: usize, 4} 5 6fn raw_from_vec_bad<T>(v: &Vec<T>) -> SliceIterBad<T> { 7 SliceIterBad { 8 ptr: v.as_ptr(), 9 len: v.len(), 10 } 11} 12 13// ❌ 能编译,但返回的 ptr 指向已释放的内存 14fn bad() -> SliceIterBad<i32> { 15 let v = vec![1, 2, 3]; 16 raw_from_vec_bad(&v) // v 在这里被 drop,但指针被带出去了 17} 18
这就是典型的“类型系统没被告知借用关系 → 编译器无法阻止悬垂”。
好例子:用 PhantomData<&'a T> 绑定借用,错误在编译期暴露
1use std::marker::PhantomData; 2 3struct SliceIter<'a, T> { 4 ptr: *const T, 5 len: usize, 6 _marker: PhantomData<&'a T>, 7} 8 9fn raw_from_vec<'a, T>(v: &'a Vec<T>) -> SliceIter<'a, T> { 10 SliceIter { 11 ptr: v.as_ptr(), 12 len: v.len(), 13 _marker: PhantomData, 14 } 15} 16 17// ❌ 这次会直接编译失败:你试图返回一个借用了局部变量 v 的值 18fn good_but_wont_compile() -> SliceIter<'static, i32> { 19 let v = vec![1, 2, 3]; 20 raw_from_vec(&v) 21} 22
你会得到类似这样的错误(不同版本文案略有差异):
1error[E0515]: cannot return value referencing local variable `v` 2 returns a value referencing data owned by the current function 3
这就达到了目的:把潜在的悬垂指针风险提前变成编译错误。
正确使用方式是:让迭代器不超过数据的作用域,例如:
1fn ok_usage() { 2 let v = vec![1, 2, 3]; 3 let it = raw_from_vec(&v); 4 // 在 v 的生命周期内使用 it 5 let _ = it.len; 6} 7
2. 绑定类型参数:FFI 句柄是 *mut (),用 PhantomData<R> 防止把 A 当 B 用
另一类常见场景来自 FFI:外部库可能用统一的 void*(Rust 里常见 *mut () 或 *mut c_void)当作“资源句柄”。运行时只有一个指针,但它背后可能对应不同资源类型。
如果你只写成“无类型句柄包装”,编译器分不清“这是 Foo 资源还是 Bar 资源”,于是非常容易混用。
坏例子:句柄不带类型信息,混用能编译,运行时才爆炸
下面用 assert! 模拟“用错句柄就炸”(真实 FFI 里可能是崩溃/数据错乱/UB):
1use std::ffi::c_void; 2 3mod foreign_lib { 4 use super::c_void; 5 6 struct Raw { 7 tag: u32, // 1 => Foo, 2 => Bar 8 } 9 10 pub unsafe fn new(tag: u32) -> *mut c_void { 11 Box::into_raw(Box::new(Raw { tag })) as *mut c_void 12 } 13 14 pub unsafe fn do_foo(handle: *mut c_void) { 15 let raw = handle as *mut Raw; 16 assert!((*raw).tag == 1, "expected Foo handle, got tag={}", (*raw).tag); 17 } 18 19 pub unsafe fn do_bar(handle: *mut c_void) { 20 let raw = handle as *mut Raw; 21 assert!((*raw).tag == 2, "expected Bar handle, got tag={}", (*raw).tag); 22 } 23 24 pub unsafe fn free(handle: *mut c_void) { 25 drop(Box::from_raw(handle as *mut Raw)); 26 } 27} 28 29struct ExternalResourceBad { 30 handle: *mut c_void, 31} 32 33impl ExternalResourceBad { 34 fn new_foo() -> Self { 35 Self { handle: unsafe { foreign_lib::new(1) } } 36 } 37 fn new_bar() -> Self { 38 Self { handle: unsafe { foreign_lib::new(2) } } 39 } 40 41 fn do_foo(&self) { unsafe { foreign_lib::do_foo(self.handle) } } 42 fn do_bar(&self) { unsafe { foreign_lib::do_bar(self.handle) } } 43} 44 45impl Drop for ExternalResourceBad { 46 fn drop(&mut self) { 47 unsafe { foreign_lib::free(self.handle) } 48 } 49} 50 51fn main() { 52 let r = ExternalResourceBad::new_bar(); 53 54 // ❌ 逻辑错误:拿 Bar 的句柄去当 Foo 用 55 // 编译器看不出来(类型都一样),但运行时可能 panic/崩溃 56 r.do_foo(); 57} 58
好例子:用 PhantomData<R> 把句柄“绑定到类型”,混用直接编译错误
1use std::{ffi::c_void, marker::PhantomData}; 2 3struct Foo; 4struct Bar; 5 6trait ResType { const TAG: u32; } 7impl ResType for Foo { const TAG: u32 = 1; } 8impl ResType for Bar { const TAG: u32 = 2; } 9 10struct ExternalResource<R> { 11 handle: *mut c_void, 12 _type: PhantomData<R>, 13} 14 15impl<R: ResType> ExternalResource<R> { 16 fn new() -> Self { 17 Self { 18 handle: unsafe { foreign_lib::new(R::TAG) }, 19 _type: PhantomData, 20 } 21 } 22} 23 24impl<R> Drop for ExternalResource<R> { 25 fn drop(&mut self) { 26 unsafe { foreign_lib::free(self.handle) } 27 } 28} 29 30fn takes_foo(_: ExternalResource<Foo>) {} 31 32fn main() { 33 let foo = ExternalResource::<Foo>::new(); 34 let bar = ExternalResource::<Bar>::new(); 35 36 takes_foo(foo); // ✅ OK 37 38 // takes_foo(bar); 39 // ❌ 编译期报错: 40 // expected `ExternalResource<Foo>`, found `ExternalResource<Bar>` 41} 42
这就是文档里“未使用类型参数”的核心意义:哪怕结构体里根本没有存 R,你仍然可以用 PhantomData<R> 让类型系统记住并区分它,从而把很多“本来只能靠人肉保证的约定”变成编译器可检查的约束。 from Pomelo_刘金,转载请注明原文链接。感谢!
《Rust 的
PhantomData:零成本把“语义信息”交给编译器》 是转载文章,点击查看原文。
