为实现DerefMut的类型实现!Unpin后Pin约束是否失效?代码运行异常原因分析
让我们一步步拆解你的问题,先从Pin的核心约束说起,再逐个分析两段代码的行为:
先明确Pin的核心规则
对于实现!Unpin的类型,当你通过Pin::new_unchecked将其固定后,你必须保证该值的内存位置永远不会被移动——这是调用new_unchecked时的强制不变式,违反它会导致未定义行为。
- Pin约束的是整个值的内存位置,而非内部字段的移动(内部字段只要不导致整个值的位置变化,是可以修改甚至移动的,只要符合类型自身规则)。
- 你无法直接获取
!Unpin类型的&mut T引用(除非通过unsafe绕过),这是Pin阻止值被移动的关键机制。
第一段代码的问题分析
你的核心误解在于指针访问的有效性和swap的对象:
无效指针访问的未定义行为
你在创建xp前保存了ptr1 = &x1.b as *const _ as isize,但随后x1被移动到Pin<EvilNUnpin>中(Pin::new_unchecked(x1)会获取x1的所有权,原x1的栈位置不再是有效的EvilNUnpin实例)。后续通过unsafe访问ptr1指向的内存属于未定义行为——Rust编译器可以任意优化这种情况,你看到的"值仍为1"只是偶然结果,完全不可依赖。swap的对象不是固定值本身
xp.as_mut()返回的是Pin<&mut EvilNUnpin>(对固定值的可变引用的Pin包装),std::mem::swap(&mut xp.as_mut(), &mut xp2.as_mut())交换的是这两个Pin<&mut EvilNUnpin>实例,而非底层的EvilNUnpin值。因为EvilNUnpin是!Unpin,你无法直接获取&mut EvilNUnpin来swap它的值(否则就违反了Pin的不变式)。
底层的EvilNUnpin值的内存位置从未被移动,它们的内部b字段被DerefMut修改为"3",但原x1/x2的栈位置早已无效,你的指针访问没有意义。
第二段代码的问题分析
同样,你误解了swap的对象和Pin的约束范围:
swap的仍然是Pin引用包装
std::mem::swap交换的是Pin<&mut EvilNUnpin<'_>>实例,也就是交换了哪个Pin引用哪个EvilNUnpin值,但EvilNUnpin值本身因为是!Unpin,内存位置没有被移动。内部引用的指向未改变
每个EvilNUnpin实例的b字段持有对s1或s2的可变引用,swap Pin引用包装后:xp现在引用原来的x2(持有s2的引用),调用deref_mut会给s2push'a',变成"2a"xp2引用原来的x1(持有s1的引用),调用deref_mut会给s1push'a',变成"1a"
所以s1和s2的最终值符合实际运行结果,你的预期错误是因为误以为swap会交换EvilNUnpin值本身,从而交换它们内部的引用指向,但实际上!Unpin约束阻止了EvilNUnpin值的移动。
补充:如何真正测试违反Pin不变式的情况
如果想要测试违反Pin::new_unchecked约束的行为,你需要直接移动!Unpin类型的值(比如通过unsafe获取&mut EvilNUnpin然后swap,或者手动拷贝内存),但这会导致未定义行为,Rust不保证任何结果。你的代码中并没有真正移动!Unpin值的内存位置,只是交换了对它们的引用的Pin包装,所以Pin的约束并没有被违反,结果符合规则。
内容的提问来源于stack exchange,提问作者calvin

