第一次读 Pin 的文档,很容易把它理解成一个容器:把值放进去,它就固定住了。实际上 Pin<P> 只是对指针 P 的一层包装,它提供的保证是契约性的:只要 P::Target: !Unpin,就没有安全代码能在 Pin 的有效期内拿到 &mut T 并把它移走。
为什么需要它
Rust 默认的移动语义是 memcpy。一个自引用结构体,如果内部有一个指针指向自己的字段,移动之后指针就悬垂了:
struct SelfRef { data: String, ptr: *const u8, // 指向 data 内部的某个字节}编译器无法证明 ptr 指向的是自己,移动 SelfRef 会让 ptr 失效。Pin 的作用是让这种类型在构造之后不能通过安全接口被移动,从而允许 unsafe 代码依赖“地址不变”这个前提。
Unpin 是逃生舱
绝大多数类型都实现了 Unpin,意思是“移动它没关系”。对 Unpin 类型,Pin<&mut T> 可以安全地拿到 &mut T:
fn get_mut<T: Unpin>(pin: Pin<&mut T>) -> &mut T { // 对 Unpin 类型,Pin 不提供额外保证,因此可以拆掉 unsafe { Pin::get_unchecked_mut(pin) }}一个类型是否 Unpin 是自动推导的:只要所有字段都 Unpin,它自己就是 Unpin。PhantomPinned 是一个反例标记,加上它可以让结构体变成 !Unpin:
use std::marker::PhantomPinned;
struct SelfRef { data: String, ptr: *const u8, _pin: PhantomPinned,}结构体钉住与投影
Pin<&mut SelfRef> 只钉住了最外层。如果 SelfRef 的某个字段需要被单独访问,必须做结构钉住投影:对 !Unpin 的字段,投影出的 Pin<&mut Field> 才能继续保持“地址不变”的保证。手写投影需要 unsafe,这也是 pin-project 这类 crate 存在的原因。
impl SelfRef { fn data(self: Pin<&mut Self>) -> Pin<&mut String> { // 字段 data 本身是 Unpin,可以直接投影 unsafe { self.map_unchecked_mut(|s| &mut s.data) } }}如果字段是 !Unpin,这样投影仍然是安全的,因为外层已经被钉住,字段地址不会变;但如果字段是 Unpin,Pin<&mut Field> 的保证反而更弱,需要看具体投影规则。
async future 为什么是 !Unpin
async fn 展开成一个状态机,状态里保存着跨 await 仍然存活的局部变量。如果某个局部变量借用了同一个 future 内的另一块数据,状态机就变成了自引用。因此编译器把这个状态机标记为 !Unpin,Future::poll 接收的是 Pin<&mut Self>。
这也解释了 Box::pin:把 future 放进堆上的 Box,地址固定,之后可以安全地 poll。而 tokio::spawn 要求 Future + Send + 'static,内部就是把它 Box::pin 起来。
常见错误
- 以为
Pin<&mut T>钉住了整个值。 如果T: Unpin,随时可以Pin::into_inner拿回&mut T并移动它。 - 在外面套一层
Pin就够了。 自引用字段必须通过结构钉住投影访问,否则安全代码可以通过DerefMut移动内层。 - 用
mem::replace绕过。 对!Unpin类型,Pin::get_mut不可用;一旦你用unsafe拿到&mut,就自己承担了不移动的证明义务。
结论
Pin 是一个关于地址稳定性的契约,不是物理锁。它把“不能移动”的证明责任交给类型系统,把实际保证交给 unsafe 的边界。写自引用结构体时,先问清楚哪些字段需要钉住、谁负责投影,再决定要不要引入 PhantomPinned。