自引用结构体在移动时出现问题的原因在于,数据已经移动到了其它内存里,但引用指向的还是原来的位置。既然在移动时才会出现不安全问题,那限制它的移动就好了。这就是Pin
的思路。
#Unpin
和!Unpin
Rust中有一个名为Unpin
的特征。它是一个标记特征,不提供任何方法,只表示移动该类型是安全的。
(如果你不理解何为「移动是安全的」,最好阅读我的上一篇文章《Rust的自引用(1):不安全的移动》)
编译器会为大部分类型自动实现Unpin
特征,。如果你定义的结构体所包含的类型都是Unpin
的,那编译器也会为它实现Unpin
。因此,我们才能安全地移动大部分类型。
!Unpin
则是Unpin
的负向实现(!
名为negative_impls
,表示某类型永远不会实现该特征)。你可以把!Unpin
当作另一个特征,它表示移动该类型是不安全的。Unpin
和!Unpin
是互斥的,对同一个类型,无法同时实现它们(编程中没有薛定谔的猫:))。
如果你定义的类型,在移动后可能带来安全问题,那就应该为它实现!Unpin
(比如自引用结构体)。有两种办法能实现!Unpin
:
- 使用
PhantomPinned
。带有这个类型的结构体,编译器不会为其实现Unpin
。
|
|
- 使用
impl
实现。
|
|
!Unpin
也是标记特征——它不带来任何限制,你依然可以移动它。它只是在告诉编译器:移动这种类型是不安全的。但编译器听归听,也不对它进行任何处理。
!Unpin
真正发挥作用的地方,在于它和Pin
结合使用。
#Pin
Pin
是结构体。它是一个包装类型,用来包装指针。
|
|
Pin
的效果是,当Ptr
所指向的类型是!Unpin
时,就会阻止它在内存中移动。但如果Ptr
所指向的类型是Unpin
,那就不会有任何行为限制。
这么说有点绕。可以从另一个角度理解——Pin
用来限制不安全的移动。
- 如果类型是
Unpin
,那它的移动本来就是安全的,没必要限制。 - 如果类型是
!Unpin
,那它的移动不安全,于是要限制。
Pin这个单词本身的意思就是「固定」。它要「固定」那些移动时可能出现安全问题的类型,使其不能移动。通过限制不安全的移动,来使得剩下的移动都是安全的。这跟Rust的哲学一脉相承。
#如何限制移动
前面是概念上的解释。这里从代码上,解释Pin
如何进行限制。
我的上一篇文章《Rust的自引用(1):不安全的移动》提到,你可以通过T
或&mut T
来移动值。现在来看,Pin
如何限制两点。
#T
Pin
类似一种指针。移动它,不会转移内部值的所有权,不发生移动。
|
|
你会发现,两次打印的地址都一样,这正是因为值的内存位置被固定了。如果你发现打印出0x1
,也不要惊讶,它也是地址。这只是因为Foo
内部没有真正的值,编译器做了优化。
#&mut T
另一种移动值的方式,是通过&mut T
。许多能移动值的方法,如std::mem::swap(&mut T, &mut T)
和std::mem::take(&mut T)
等,接收的参数都是&mut T
。
Pin
对此的处理是(假设是Pin<Ptr<T>>
):
- 如果
T
实现的是Unpin
,能拿到&mut T
(没有限制)。 - 如果
T
实现的是!Unpin
,不能拿到&mut T
。
对于Pin<Ptr<T>
,可以通过as_mut(&mut self)
方法,以解引用的方式转换成Pin<&mut T>
。然后再通过Pin<&mut T>
的方法get_mut(self)
,来得到&mut T
。
|
|
但get_mut(self)
存在限定
pub fn get_mut(self) -> &'a mut T
where
T: Unpin
因此,如果T
是!Unpin
的,就不能调用get_mut
方法。这样就无法通过Pin<Ptr<T>>
来得到&mut T
,更不可能调用那些接收&mut T
的方法来移动值。我们安全了。
#第三种引用
看到这里,好像Pin
只是通过暴露方法,来限制用户拿到内部值的&mut T
。看起来也没有什么特别的,我们完全能实现一个类似的类型。
而且这种设计似乎也没有什么用。拿不到&mut T
,那意味着不能使用值的&mut self
方法。难道要求我们只使用它的&self
方法?如果需要修改数据,就在方法里用unsafe代码或内部可变性(如RefCell<T>
)来修改内部的值?
当然不是这样。接下来要介绍Pin
神奇的地方了——它能作为方法的self
。
|
|
do_something
方法要通过Pin
类型才能访问:
|
|
那现在的路线就就很明确了。我们可以遵循以下方式设计一个!Unpin
类型:
- 只提供返回
Pin<Ptr<T>>
的构造方法。既然只在操作Pin<Ptr<T>>
时才能保证行为安全,那限制用户只能获得这个类型就好了。我们不允许用户创建带有所有权的T
,以阻止它进一步得到&mut T
。 - 提供
Pin<&mut Self>
方法。所有可变操作,都在这类方法上完成。 - 可以提供
&self
方法。它们是安全的,且能通过Pin<Ptr<T>>
来访问。 - 不提供
&mut self
和self
方法。它们是不安全的,而且也无法访问到。
这样,就能确保它的行为是安全的,且有足够的表达力。
我们可以将Pin<&mut T>
看作是一种特殊的引用类型。它的效果是:
- 能像
&mut T
一样修改自身 - 但不能在内存中移动数据——不能拿到
&mut T
并传给其他函数。
这种能力跟&T
和&mut T
都不同,有点介于两者之间。因此,有人说Pin<&mut T>
是Rust中的第三种引用,我表示认同。
#重新实现自引用结构体
我的上一篇文章《Rust的自引用(1):不安全的移动》中的自引用结构体实现失败了。让我们用Pin
重新实现一下。
|
|
使用它
|
|
我们无法创建SelfRef
类型的T
、&T
和&mut T
,所有的操作都是Pin<Ptr<T>>
上完成的,不会发生不安全的移动。这个实现还非常优雅,去掉了丑陋的init
方法。Rust很开心,我们也很开心。
PS:尝试在不同地方执行这个语句,打印数据的内存地址:
|
|
你会发现,地址都是一样的,因为不管怎么操作,数据都被固定在了内存的某片区域中,不能移动了。
#总结
如果一个类型实现了!Unpin
,那表示它在内存中移动时,可能出现不安全的行为。
Rust对这种类型的处理办法是,使用Pin<Ptr<T>>
来限制它在内存中的移动。无法移动,体现在Pin
不暴露&mut T
,使得一系列接收&mut T
的移动数据的方法,都无法在该类型上使用。
Pin
的另一个能力是支持编写self
为Pin<&mut Self>
的方法。这样只需要暴露Pin<Ptr<T>>
给用户即可,并不会削弱这种类型的表达能力。
目前,似乎只有自引用结构必须是!Unpin
(如果发现了其他结构也需要,请告知我!)。这种类型一般使用了指向自身的裸指针。在内存中移动它,可能出现空指针或指向非法内存,因此要配合Pin
使用。
在实际开发中,我们很少用到自引用结构体。即使需要,也往往可以用其他设计来代替,比如使用Rc
和RefCell
,让用户使用前做自行检查等等。
自引用结构体只是一种数据结构。它没有好坏之分,只是在某些场景下更适用。Rust编译器实现的Future
是自引用结构体,内部有指向自身的引用,用来记录当前状态下内部数据的复杂关系。但你完全可以其他方式来实现Future
,比如用某种数据结构来记这种关系。但这种需求无疑更适合自引用结构,性能也会更高。