Rust:内部可变性 & 型变
内部可变性
在前一节博客的末尾,我们讨论了内部可变性与协变共同导致的垂悬借用,但我并没有给出一个实际的解决方案。本节博客就深入了解内部可变性,并了解Rust是如何安全地提供内部可变性的。
当持有一个复合类型的不可变借用时,意味着我们不能修改这个类型。但是例如结构体,如果持有结构体不可变借用,意味着所有字段都是不可修改的,这种不可变的粒度太大了。而内部可变性就是指,当持有不可变借用时,依然可以修改复合类型内部的部分字段。
可以类比一些面向对象语言(尤其是Java),对于一个类内部的属性,不允许你直接进行修改和读取,必须通过指定的接口get/set进行读写,因为这些方法已经经过了安全的封装。
实现内部可变性,在Rust 1.83之前可以用原生指针实现,比如上节博客的:
rust
struct MyCell<'a> {
value: &'a i32,
}
impl<'a> MyCell<'a> {
fn set(&self, other: &'a i32) {
unsafe {
std::ptr::write(&self.value as *const &i32 as *mut &i32, other);
}
}
}
这里强行把一个&self.value的不可变借用,转化为了可变原生指针。但是在新版Rust已经禁止了这个能力。现在Rust只提供一种方案实现内部可变性,并基于这个方案实现了两个常用工具Cell和RefCell。
UnsafeCell
Cell和RefCell都是基于UnsafeCell实现的,它是Rust官方提供的内部可变性实现方案,允许你使用不可变借用来修改数据。
定义如下:
rust
#[lang = "unsafe_cell"]
#[stable(feature = "rust1", since = "1.0.0")]
#[repr(transparent)]
#[rustc_pub_transparent]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
它的内容很简单,就是一个value: T存储可变的数据。
get
get是整个UnsafeCell最核心的方法,也体现了它最根本的逻辑,代码如下:
rust
#[inline(always)]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_stable(feature = "const_unsafecell_get", since = "1.32.0")]
#[rustc_as_ptr]
#[rustc_never_returns_null_ptr]
pub const fn get(&self) -> *mut T {
// We can just cast the pointer from `UnsafeCell<T>` to `T` because of
// #[repr(transparent)]. This exploits std's special status, there is
// no guarantee for user code that this will work in future versions of the compiler!
self as *const UnsafeCell<T> as *const T as *mut T
}
这个代码其实很简单,从签名可以看出来就是get方法接受一个不可变借用&self,但是返回一个可变的原生指针*mut T。
函数体内部只有一行代码:
rust
self as *const UnsafeCell<T> as *const T as *mut T
它先获得self的原生指针*const UnsafeCell<T>,这一步是很合理的。随后把*const UnsafeCell<T>转化为了 *const T,再转化为*mut T。
这里有一个让人匪夷所思的地方,为什么*const UnsafeCell<T>可以直接变成*const T?它们根本就不是一个类型!
你可以回看UnsafeCell<T>,它有一个属性#[repr(transparent)],这个属性的含义是:结构体的内存布局,要和它内部唯一的字段一模一样。因此从内存的角度UnsafeCell<T>和T是一模一样的,所以允许这种转化。
get函数体内部官方给的注释也指出了这一点,但是用户在代码中不能这么做,它是Rust库函数的特权语法。
这个get方法是一个safe的方法,因为它只创建了原生指针,并没有使用原生指针。
其它方法
- into_inner
into_inner用于拿到内部的值,并且是以值的形式:
rust
#[inline(always)]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_stable(feature = "const_cell_into_inner", since = "1.83.0")]
#[rustc_allow_const_fn_unstable(const_precise_live_drops)]
pub const fn into_inner(self) -> T {
self.value
}
它直接以值的形式返回了self.value,这意味着如果它没有实现Copy,这会导致所有权的转移。
- replace
replace用于替换内部的数据T,并返回原本的数据。
rust
#[inline]
#[unstable(feature = "unsafe_cell_access", issue = "136327")]
pub const unsafe fn replace(&self, value: T) -> T {
unsafe { ptr::replace(self.get(), value) }
}
方法接受一个不可变借用,随后通过self.get()拿到原生指针,传入ptr::replace中。此时通过原生指针将值设置到T中,并且ptr::replace会返回原本的值,它被作为最后一个表达式从UnsafeCell::replace中返回出去。这是一个unsafe方法。
- get_mut
get_mut用于拿到内部数据的可变借用:
rust
#[inline(always)]
#[stable(feature = "unsafe_cell_get_mut", since = "1.50.0")]
#[rustc_const_stable(feature = "const_unsafecell_get_mut", since = "1.83.0")]
pub const fn get_mut(&mut self) -> &mut T {
&mut self.value
}
它要求&mut self,意味着它需要满足借用检查。在RefCell中,实现内部可变性是通过*mut T的原生指针实现的,但是如果是&mut T,它无法逃过借用检查,不允许你通过一个不可变借用&self拿到一个可变借用&mut T。
比如:
rust
let mut num = UnsafeCell::new(5);
let num_b1 = num.get_mut();
let num_b2 = num.get_mut();
*num_b1 += 1;
*num_b2 += 1;
这一段代码无法编译通过,因为在同时存在多个T的可变借用,这违背了借用规则。
而且UnsafeCell不提供方法得到内部数据的不可变借用,设想一个场景:
当用户拿到UnsafeCell内部数据的不可变借用,由于不可变借用可以同时存在,因此可以调用get方法再拿到可变原生指针。此时就破坏了借用规则,也就是共享不可变(多个不可变借用期间,不能修改数据)。
而假设用户拿到的是一个可变借用,那么用户就无法调用get方法了,因为get要求&self的借用。而已经存在一个可变借用情况下,是无法进行不可变借用的,进而保证了无法调用到get方法。
由此可见,UnsafeCell没有破坏借用规则,保证了"共享不可变,可变不共享",这是通过以下设计共同达成的:
- 提供
get接口,接受不可变借用&self,返回可变指针*mut T,达到内部可变性 - 提供
get_mut接口,接受可变借用&mut self,返回可变借用&mut T- 阻止存在可变借用期间,调用
get方法,导致多个途径可修改同一数据,破坏"可变不共享"
- 阻止存在可变借用期间,调用
- 不提供不可变借用接口
- 阻止存在不可变借用时,调用
get方法,破坏"共享不可变"
- 阻止存在不可变借用时,调用
不变性
如果只是遵循借用规则,那还远不能说UnsafeCell是一种安全的内部可变性手段。内部可变性与协变还会共同导致垂悬借用。
回看之前博客的例子:
rust
struct MyCell<'a> {
value: &'a i32,
}
impl<'a> MyCell<'a> {
fn set(&self, other: &'a i32) {
unsafe {
std::ptr::write(&self.value as *const &i32 as *mut &i32, other);
}
}
}
fn func<'b>(c: &MyCell<'b>) {
let tmp: i32 = 2025;
c.set(&tmp);
println!("change value: {}", c.value);
}
static X: i32 = 2005;
fn main() {
let cell = MyCell { value: &X };
func(&cell);
println!(" end value: {}", cell.value);
}
这段代码由于垂悬借用,会导致未定义行为。因为MyCell具有内部可变性,同时还具有协变的性质。导致了MyCell<'static>可以替代MyCell<'a>,编译器发生生命周期的误判。如果你无法理解这个过程,请回看上一篇博客的详细分析。
UnsafeCell不是一个协变类型,而是一个不变类型
UnsafeCell带有一个属性#[lang = "unsafe_cell"],这也是Rust内部的特殊标记,这直接修改了UnsafeCell的型变类型,从一个协变类型变成了不变类型。
示例:
rust
fn test_cell<'l: 's, 's>(mut left: UnsafeCell<&'s i32>, right: UnsafeCell<&'l i32>) {
left = right;
}
函数接受两个生命周期'l和's,其中'l: 's意味着'l生命周期更长,型变角度就是'l <: 's。
假设UnsafeCell是一个协变类型,就有UnsafeCell<&'l i32> <: UnsafeCell<&'s i32>,那么以上代码是合法的,编译器检查可以通过。
但是以上代码会报错,因为UnsafeCell是不变类型,意味着UnsafeCell<&'l i32>和UnsafeCell<&'s i32>没有任何关系。即使确实'l生命周期更长,但是经过UnsafeCell包装后,这两个生命周期没有任何关系!
这才是UnsafeCell最关键的一点,它破坏了协变,从而保证用户在使用内部可变性的同时,一定不会触发协变,进而不会触发垂悬借用。
总的来看,UnsafeCell既提供了内部可变性,又遵循了借用规则,还破坏了协变。从各方面来说,它都称得上一个安全的内部可变性方案。
那为什么还要叫做UnsafeCell?原因在于,使用不可变的借用,得到了一个可变的原生指针,这可能导致一些问题。等到用户使用get返回的指针时,就需要用到unsafe操作了。
实际上在需要内部可变性时,不会直接使用UnsafeCell,而是使用它的基础上封装出的Cell和RefCell。
Cell
Cell实现了最基本的内部可变性,适用于实现了Copy的类型,实现在std::cell中。
它的定义如下:
rust
pub struct Cell<T: ?Sized> {
value: UnsafeCell<T>,
}
这个结构体很简单,只有一个value,它是UnsafeCell<T>。
- new
构造一个Cell非常简单,它实现了new方法:
rust
Cell::new(10);
Cell::new("hello");
这样就把一个类型包裹进了Cell中。
- get
通过get方法,可以拿到Cell内部的值:
rust
let num_cell = Cell::new(5);
let num = num_cell.get();
println!("The number is: {}", num);
要注意,这个方法只有实现了Copy的T可以使用。它最后其实是调用了self.value.get(),这个方法之前讲解过,返回的是一个值表达式,因此如果是非Copy类型,会导致内部数据的所有权直接被转移走。Cell直接禁止你使用非Copy的类型调用,保证所有权留在Cell内部。
- replace
通过replace方法,可以替换Cell内部的值,源码如下:
rust
#[inline]
#[stable(feature = "move_cell", since = "1.17.0")]
#[rustc_const_stable(feature = "const_cell", since = "1.88.0")]
#[rustc_confusables("swap")]
pub const fn replace(&self, val: T) -> T {
mem::replace(unsafe { &mut *self.value.get() }, val)
}
它没有去调用UnsafeCell::replace,而是亲自去调用mem::replace。因为UnsafeCell::replace是一个unsafe的方法,而Cell需要封装为一个安全的接口,所以使用了mem::replace进行安全的替换操作。这个接口与ptr::replace不同,它操作的是借用而非原生指针,借用检查就有了用武之地,因此更加安全。
同样的,它最后返回了T,这会导致所有权的转移,或者发生拷贝。
- set
通过set方法,可以修改Cell内部的值,源码如下:
rust
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn set(&self, val: T) {
self.replace(val);
}
这个方法对于任意的T都可以使用,哪怕不是Copy的类型。它调用了自己的self.replace,注意不是self.value.replace。这个方法会返回内部的值表达式,而set中没有用任何变量接收这个返回值,这意味着如果T是一个非Copy类型,会在replace返回的同时将原本的数据进行析构,防止内存泄露。
- get_mut
通过get_mut方法,可以拿到内部值的可变借用:
rust
let mut num_cell = Cell::new(5);
let r = num_cell.get_mut();
*r = 10;
println!("{}", num_cell.get());
这个方法调用的就是UnsafeCell::get_mut,拿到内部值的可变借用,如果T是非Copy类型,那么就需要通过这种方式拿到内部的数据。
以上三个就是Cell最核心的接口,它对UnsafeCell进行了包装,提供了固定的方法让使用内部可变性。
Cell是对UnsafeCell的安全封装,比如get方法直接拿到数据拷贝而非*mut T原生指针,replace方法也替换为了更加安全的版本。在这些基础上,又继承了UnsafeCell的型变机制以及对借用检查的兼容。所以在需要内部可变性时往往不使用UnsafeCell而是Cell。
RefCell
在Cell中,只有Copy的类型可以使用get,否则只能通过可变借用修改数据。但是如果需要对数据进行多份不可变借用,那Cell用起来确实比较别扭。
但是一旦提供可变借用的手段,那么就可能破坏借用规则。为此Rust又封装了RefCell,它允许用户持有内部数据的不可变借用,这确实破坏了编译期的借用规则,但是它内部自己维护了一套运行时借用检查,保证不会发生内存错误。
定义如下:
rust
type BorrowCounter = isize;
#[rustc_diagnostic_item = "RefCell"]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct RefCell<T: ?Sized> {
borrow: Cell<BorrowCounter>,
value: UnsafeCell<T>,
#[cfg(feature = "debug_refcell")]
borrowed_at: Cell<Option<&'static crate::panic::Location<'static>>>,
}
它有三个属性:
borrow:这是一个Cell<isize>,用Cell提供内部可变性刚刚好,这是一个计数,用于在运行时统计借用情况value:这是实际存储的数据borrowed_at:这个字段用于统计第一次借用位置
这个borrowed_at比较复杂,它也通过Cell保证内部可变性,Option表示为空的情况,最内部是一个crate::panic::Location,这个Location是一种文件元信息,存储了文件名,行号,列号。当运行时发生违背借用规则的行为,panic!就会输出这个Location内部的信息,帮助程序员定位借用错误。
borrow是一个计数器,它也是整个RefCell运行时借用检查的核心。初始borrow = 0表示没有任何借用。
- 进行可变借用:检查
borrow == 0,表示当前没有任何借用,借用完毕后borrow = -1 - 进行不可变借用:检查
borrow >= 0,表示当前不存在可变借用,borrow += 1,当前borrow的值是多少,就说明有多少个不可变借用
光说规则显得很晦涩,其实它的机制很简单,通过接口来深入了解它是如何达到运行时借用检查的。
Ref
当尝试从RefCell中得到一个不可变借用,实际拿到的不是&T,而是Ref<T>,它是经过封装的不可变借用。
rust
#[stable(feature = "rust1", since = "1.0.0")]
#[must_not_suspend = "holding a Ref across suspend points can cause BorrowErrors"]
#[rustc_diagnostic_item = "RefCellRef"]
pub struct Ref<'b, T: ?Sized + 'b> {
value: NonNull<T>,
borrow: BorrowRef<'b>,
}
value指向的具体数据,NonNull防止为空,内部存储的是原生指针*const Tborrow是引用计数
BorrowRef的结构如下:
rust
struct BorrowRef<'b> {
borrow: &'b Cell<BorrowCounter>,
}
他内部是一个借用 &Cell<BorrowCounter>,实际上指向了某个RefCell内部的counter。
当BorrowRef析构时,引用计数会自减:
rust
#[rustc_const_unstable(feature = "const_ref_cell", issue = "137844")]
impl const Drop for BorrowRef<'_> {
#[inline]
fn drop(&mut self) {
let borrow = self.borrow.get();
debug_assert!(is_reading(borrow));
self.borrow.replace(borrow - 1);
}
}
在drop中,先拿到内部的计数值BorrowCounter,随后使用replace接口将borrow - 1替换进去,实现了析构时自动减少计数。
当BorrowRef创建时,引用计数会自增:
rust
const UNUSED: BorrowCounter = 0;
#[inline(always)]
const fn is_reading(x: BorrowCounter) -> bool {
x > UNUSED
}
impl<'b> BorrowRef<'b> {
#[inline]
const fn new(borrow: &'b Cell<BorrowCounter>) -> Option<BorrowRef<'b>> {
let b = borrow.get().wrapping_add(1);
if !is_reading(b) {
None
} else {
borrow.replace(b);
Some(BorrowRef { borrow })
}
}
}
通过new创建一个BorrowRef,需要传入一个&Cell<BorrowCounter>。
首先borrow.get().wrapping_add(1)对这个计数器进行自增处理,然后进入is_reading判断当前是否有可变借用。如果有可变借用,那么返回None表示创建失败。如果没有则把原数据更新为自增后的值,最后返回创建好的BorrowRef 。
在 is_reading 判断x > 0。如果已经存在可变借用,那么原本的值就是-1。自增后为0,无法满足条件,说明当前有借用正在读取。反之如果大于0,说明当前没有可变借用,要么这是第一次创建,要么有别的不可变借用。
因此BorrowRef在new的时候增加引用计数,在drop的时候减少引用计数,这是基于RAII机制动态维护借用数量。
了解了Ref的机制后,看看RefCell是如何创建不可变借用的。
- try_borrow
borrow尝试获得一个不可变借用,返回一个Result,源码如下:
rust
#[stable(feature = "try_borrow", since = "1.13.0")]
#[inline]
#[cfg_attr(feature = "debug_refcell", track_caller)]
#[rustc_const_unstable(feature = "const_ref_cell", issue = "137844")]
pub const fn try_borrow(&self) -> Result<Ref<'_, T>, BorrowError> {
match BorrowRef::new(&self.borrow) {
Some(b) => {
#[cfg(feature = "debug_refcell")]
{
if b.borrow.get() == 1 {
self.borrowed_at.replace(Some(crate::panic::Location::caller()));
}
}
let value = unsafe { NonNull::new_unchecked(self.value.get()) };
Ok(Ref { value, borrow: b })
}
None => Err(BorrowError {
#[cfg(feature = "debug_refcell")]
location: self.borrowed_at.get().unwrap(),
}),
}
}
try_borrow将自己的self.borrow传入BorrowRef::new创建一个借用,刚刚已经介绍过在new内部会自动检查借用,最后对返回值进行模式匹配。
如果是Some,说明创建不可变借用成功。如果b.borrow.get() == 1,说明这是第一次进行借用,将借用位置的数据存储到borrowed_at中,方便后续错误处理。
随后通过self.value.get()拿到裸指针*mut T,并把这个指针包装袋NonNull里面,这表示这个指针一定不能为空,这是一层增强安全性的包装。最后返回Ok(Ref { value, borrow: b }),这是在将指向原数据的非空指针和引用计数包装为Ref返回给用户使用。
但是如果违反了借用检查,也就是最开始的模式匹配为None,此时就返回一个错误,错误中包裹的信息是borrowed_at,也就是第一次发生借用的位置,用户就可以定位错误。
- borrow
borrow可以直接拿到一个不可变借用,而非得到一个Result。但是如果违背了借用检查,就会panic!导致程序崩溃。
源码:
rust
#[stable(feature = "rust1", since = "1.0.0")]
#[inline]
#[track_caller]
#[rustc_const_unstable(feature = "const_ref_cell", issue = "137844")]
pub const fn borrow(&self) -> Ref<'_, T> {
match self.try_borrow() {
Ok(b) => b,
Err(err) => panic_already_mutably_borrowed(err),
}
}
在borrow内部,直接复用了try_borrow的逻辑,最后对返回的Result进行模式匹配。如果是Ok就把内部的Ref直接返回,反之则基于错误信息panic!。
RefMut
当对RefCell进行可变借用,拿到的也不是&mut T,而是RefMut<T>。
结构如下:
rust
#[stable(feature = "rust1", since = "1.0.0")]
#[must_not_suspend = "holding a RefMut across suspend points can cause BorrowErrors"]
#[rustc_diagnostic_item = "RefCellRefMut"]
pub struct RefMut<'b, T: ?Sized + 'b> {
value: NonNull<T>,
borrow: BorrowRefMut<'b>,
marker: PhantomData<&'b mut T>,
}
value和borrow字段与之前的Ref功能是相同的,一个用于存储数据,另一个用于维护借用检查。此外还有一个marker,他是PhantomData类型,这个会在稍后讲解,它可以将RefMut的型变改为不变。
与BorrowRef相同,BorrowRefMut基于RAII机制,在new和drop时对借用计数进行修改:
rust
struct BorrowRefMut<'b> {
borrow: &'b Cell<BorrowCounter>,
}
const UNUSED: BorrowCounter = 0;
impl<'b> BorrowRefMut<'b> {
#[inline]
const fn new(borrow: &'b Cell<BorrowCounter>) -> Option<BorrowRefMut<'b>> {
match borrow.get() {
UNUSED => {
borrow.replace(UNUSED - 1);
Some(BorrowRefMut { borrow })
}
_ => None,
}
}
}
#[rustc_const_unstable(feature = "const_ref_cell", issue = "137844")]
impl const Drop for BorrowRefMut<'_> {
#[inline]
fn drop(&mut self) {
let borrow = self.borrow.get();
debug_assert!(is_writing(borrow));
self.borrow.replace(borrow + 1);
}
}
但是不同的是,因为-1表示不可变借用。在new时BorrowRefMut把计数自减,而drop时把计数自增,与BorrowRef刚好相反。
new时判断当前的计数是不是0,如果是,说明当前不存在任何借用,通过borrow.replace(UNUSED - 1)把计数设置为-1。
- try_borrow_mut
try_borrow_mut尝试获得一个可变借用,返回Result,源码如下:
rust
#[stable(feature = "try_borrow", since = "1.13.0")]
#[inline]
#[cfg_attr(feature = "debug_refcell", track_caller)]
#[rustc_const_unstable(feature = "const_ref_cell", issue = "137844")]
pub const fn try_borrow_mut(&self) -> Result<RefMut<'_, T>, BorrowMutError> {
match BorrowRefMut::new(&self.borrow) {
Some(b) => {
#[cfg(feature = "debug_refcell")]
{
self.borrowed_at.replace(Some(crate::panic::Location::caller()));
}
let value = unsafe { NonNull::new_unchecked(self.value.get()) };
Ok(RefMut { value, borrow: b, marker: PhantomData })
}
None => Err(BorrowMutError {
#[cfg(feature = "debug_refcell")]
location: self.borrowed_at.get().unwrap(),
}),
}
}
这段代码逻辑和当时try_borrow几乎一模一样。通过BorrowRefMut::new(&self.borrow)直接创建一个BorrowRefMut,在new内部进行借用检查,对返回值进行模式匹配。
如果是Some,就记录借用位置borrowed_at,因为可变借用是独占的,所以无需进行if判断,直接记录。最后把原生指针取出来包装到NonNull中,最后构造一个RefMut包装到Ok中返回。
如果是None,说明违反了借用规则,此时将第一次借用的记录borrow_at包装在Err内部返回。
- borrow_mut
borrow_mut用于获得一个可变借用,如果违反借用规则就panic!,源码如下:
rust
#[stable(feature = "rust1", since = "1.0.0")]
#[inline]
#[track_caller]
#[rustc_const_unstable(feature = "const_ref_cell", issue = "137844")]
pub const fn borrow_mut(&self) -> RefMut<'_, T> {
match self.try_borrow_mut() {
Ok(b) => b,
Err(err) => panic_already_borrowed(err),
}
}
这个也和borrow几乎一模一样,就是复用了try_borrow_mut的逻辑。如果是Ok就把内部的RefMut拆出来返回,反之则panic!。
最后可以通过以下逻辑图回顾一下:

图中蓝色代表我们直接操作的结构体,黄色是结构体内部的属性,绿色则是相关方法。不得不说,RefCell的设计还是十分精妙的,逻辑简约而严谨。
通过borrow和borrow_mut方法分别可以获取不可变借用Ref与可变借用Ref,这两个借用内部维护了指向数据的指针NonNull<*const T>,以及引用计数器BorrowRef和BorrowRefMut。引用计数器基于RAII机制,在new时进行借用检查并修改计数,drop时恢复计数。
另外的,借用内部存NonNull<T>,而NonNull内部实际存储了原生指针*const T,这是为了避免编译器的借用检查,将RefCell内部的借用检查完全由运行时负责。
最后,Ref和RefMut分别实现Deref和DetefMut,它们可以直接当做&T和&mut T使用。关于这两个Trait会在智能指针章节介绍。
讲完了理论,展示一下用法:
rust
fn main() {
let data = RefCell::new(42);
// 多个不可变借用(Ref<i32>)
{
let r1 = data.borrow(); // 第一个 Ref
let r2 = Ref::clone(&r1); // 克隆出第二个 Ref
println!("r1 = {}, r2 = {}", *r1, *r2); // 当普通 &T 用
} // r1, r2 在这里 drop,借用计数归零
// 独占可变借用(RefMut<i32>)
{
let mut rm = data.borrow_mut(); // RefMut 独占借用
*rm += 1; // 当普通 &mut T 用
println!("rm = {}", *rm);
} // drop,释放独占借用
// 再次只读借用,看到修改后的值
{
let r = data.borrow();
println!("after mutation = {}", *r);
}
}
前面的理论确实枯燥,用起来真的非常简单。Ref<T>直接当做&T,而RefMut<T>直接当做&mut T,外层看起来几乎没有任何区别。
常见型变
之前说过,Rust大部分结构体都是协变,但也有不少其它类型是不变或逆变,接下来梳理一下Rust中常见类型的型变。
重要的型变:
&'a T:在'a和T上是协变,对应的*const T也是协变&'a mut T: 在'a上是协变,但是在T上是不变fn(T) -> U: 在参数T上是逆变/不变,在返回值U上是协变- 内部可变类型:在
T上都是不变,对应的*mut T也是不变,比如UnsafeCell<T>、Cell<T>、RefCell<T>
上面这一段看起来有点懵,什么叫做在'a是协变,在T是不变,这都是些啥啊?
接下来用案例一个一个分析。
&'a T
&'a T:在'a和T上是协变,对应的*const T也是协变
接下来都使用以下结构体做案例:
rust
struct MyStruct<'inner> {
s: &'inner str,
}
这是一个含有内部借用的结构体。
- 在
'a协变:意味着'a可以从一个长生命周期变为短生命周期
rust
// &'a T 在 'a 协变
fn variance_ref_lifetime<'long: 'short, 'short>(x: &'long i32)
{
let _: &'short i32 = x;
}
以上代码编译通过,'long生命周期长于'short,所以存在'long <: 'short关系。
因为&'a T在'a协变,所以 &'long i32 <: &'short i32。子类型可以作为父类型使用,以上代码中将长生命周期赋值给短生命周期变量就是成立的,编译通过。
- 在
T协变:意味着T内部的生命周期可以用长生命周期变为短生命周期
rust
// &'a T 在 T 协变
fn variance_ref_T<'long: 'short, 'short>(x: &MyStruct<'long>)
{
let y: &MyStruct<'short> = x;
}
代码同样可以编译通过。
因为&'a T在T协变,所以 &MyStruct<'long> <: &MyStruct<'short>,此处的T就是MyStruct。以上x到y的赋值就是合法的。
所以说,'a协变意味着生命周期本身的传递关系,而T协变意味着T内部包含生命周期时的关系传递。
&'a mut T
&'a mut T: 在'a上是协变,但是在T上是不变
- 在
'a协变:意味着'a可以从一个长生命周期变为短生命周期
rust
// &'a mut T 在 'a 协变
fn variance_refmut_lifetime<'long: 'short, 'short>(x: &'long mut i32)
{
let y: &'short mut i32 = x;
}
以上代码编译成功,原因和&'a T一样,'a位置是协变的。
- 在
T不变:意味着内部含有不同生命周期的T之间不能以任何形式互相传递
rust
// &'a mut T 在 T 不变
fn variance_refmut_T<'long: 'short, 'short>(x: &mut MyStruct<'long>)
{
let y: &mut MyStruct<'short> = x;
}
这段代码会报错,分析一下。
与之前相同的是存在'long <: 'short关系,但由于&mut MyStruct在MyStruct是不变的,因此&mut MyStruct<'long>和&mut MyStruct<'short>之间没有任何关系。你不能把x赋值给y,它们两个类型关系是不变而非协变。
有人就要问了,为什么?明明把长生命周期变为短生命周期,可以保证在y期间整个生命周期都是合法的啊?问题在于这可能导致垂悬借用。
思考一个问题,&mut T是可变借用,意味着可以通过这个借用修改T内部的字段,因此它具有内部可变性。一旦T还具有协变的性质,直接就会导致垂悬借用。
例如:
rust
fn dangerous<'long: 'short, 'short>(mut_ref: &'long mut MyStruct<'long>, temp_str: &'short str)
{
let shorter_mut: &mut MyStruct<'short> = mut_ref;
shorter_mut.s = temp_str;
}
fn main() {
let mut ms = MyStruct { s: "hello" };
{
let s = String::from("shorter");
dangerous(&mut ms, s.as_str());
}
println!("s: {:?}", ms.s);
}
这段代码,通过一个dangerous修改 MyStruct 内部的借用s。
外部的ms生命周期是'longer,内部temp_str的生命周期是'short。那么x的具体类型就是&mut MyStruct<'longer>,而shorter_mut尝试通将 &mut MyStruct<'longer> 转换为类型 &mut MyStruct<'short>,假设它是协变,那么这行代码可以通过。
随后shorter_mut.s = temp_str,这是把内部的s重新赋值为temp_str。由于两者的生命周期都是'short,所以这行代码没有问题。但是实际上此处发生了长生命周期被短生命周期覆盖,也就是所谓的协变,导致编译器的误判。
最后在main中输出,但是此时内部的字符串已经销毁了,产生未定义行为。
还好以上行为并不会发生,因为在shorter_mut创建的时候,就已经被编译器找到问题了。因为&'a mut T中的T是不变,你根本不能把 &mut MyStruct<'longer> 转换为 &mut MyStruct<'short>。
为什么&'a T中T是协变,但是&'a mut T中却是不变?就在于mut提供了修改能力,如果允许长生命周期被短生命周期覆盖,修改的时候就可以把短生命周期的借用覆盖原本长生命周期的借用,这就会导致垂悬。但是如果只是读取,那长生命周期变短是没有问题的。
最后提一嘴,原生指针*const T对于T是协变的,*mut T对于T是不变的。
fn(T) -> U
fn(T) -> U: 在参数T上是逆变/不变,在返回值U上是协变
此处 fn(T) -> U 是一个函数指针,曾经简单讲过,它内部存储的是函数的实际地址,可以直接用它调用函数。
- 在参数
T上是逆变/不变:
rust
fn print_str(s: &'static str) {
println!("{}", s);
}
fn takes_short(f: fn(&str)) {
let s = String::from("hi");
f(&s);
}
fn main() {
takes_short(print_str); // 编译错误
}
这段代码虽然简单,但是需要绕个弯子来分析。
print_str是一个函数,它接受一个&'static str,用于输出字符串。
而takes_short则接受一个函数指针,内部创建一个临时字符串,尝试用这个内部字符串调用函数。
最后在main中,把print_str作为函数指针传给takes_short,这段代码会报错。
带上生命周期,print_str的类型就是fn(&'static str),现在尝试把它传给fn(&str),假设&str的生命周期是'a。'a一定小于等于'static,因为'static是最长的生命周期。因此存在'static <: 'a的关系。但是由于fn(T)对于T是逆变的,此处传递失败,编译错误,你不能把fn (&'static str)转换为fn (&'a str)。
思考一个问题,为什么这里不允许把一个长生命周期转为短生命周期?明明只是一个只读借用。
因为这其实是一个逆向的过程,把print_str这个函数指针传给takes_short,实际上是takes_short反过来调用print_str函数。对于一个需要&'static str的函数,takes_short却尝试传一个'a生命周期的参数进去,生命周期不够长,所以报错。这里要绕个弯子,可以多思考一下。
以上代码编译不通过,只能证明T不是协变,还不能证明它是逆变,因此再看一个例子:
rust
fn print_str(s: &str) {
println!("{}", s);
}
fn takes_static(f: fn(&'static str)) {
f("Hello");
}
fn main() {
takes_static(print_str);
}
这段代码和刚才类似。不过print_str的类型变成了fn(&str),而takes_static需要的函数指针变成了fn(&'static str)。
在main中调用takes_static。假设print_str参数的生命周期为'a,那么print_str就是fn(&'a str)。现在尝试把fn(&'a str)作为fn(&'static str)类型使用,'a一定小于等于'static,直觉上一个短生命周期怎么可能当做长生命周期用呢?
这里编译通过了,因为fn(T)对于T是逆变。因此存在fn('a) <: fn('static),那么fn('a)就可以当做fn('static)使用。
从逻辑上来理解,其实是takes_static在内部调用了print_str函数。print_str需要一个生命周期'a,而takes_static传了一个'static进去,长生命周期替代短生命周期,这可太合理了。这是和刚才同样的逆向思考过程。
要注意,目前fn(T)对T是唯一存在逆变的情况,未来有可能从逆变变为不变。
- 在返回值
U上是协变
如果理解了前面的,这个就更好理解了。
rust
fn make_long() -> &'static str {
"Long life"
}
fn takes_short<'short>(f: fn() -> &'short str) {
let ret = f();
println!("returned: {}", ret);
}
fn main() {
takes_short(make_long);
}
函数make_long返回了一个&'static str,整体的类型是fn () -> &'static str。而takes_short接受一个函数指针fn() -> &'short str。
在main函数中,尝试把make_long作为参数传给takes_short。也就是把fn () -> &'static str转化为fn() -> &'short str,这是合法的,因为fn() -> U在U是协变,存在关系fn() -> &'static str <: fn() -> &'short str,转化合法。
从逻辑上思考,takes_short调用make_long是为了拿到一个'short生命周期的借用,但是make_long返回了一个'static借用,生命周期更长了,这不是更加安全了吗?
内部可变类型
凡是内部可变类型,生命周期都是不变,因为内部的UnsafeCell是不变,这个就不细讲了。
结构体型变规则
我之前说,结构体大部分都是协变,实际上它有独立的规则,这取决于内部的字段。
例如:
rust
struct MyStruct<'a, T> {
r: &'a T,
}
由于&'a T中'a是协变,T也是协变,所以MyStruct<'a, T>中'a是协变,T也是协变。
当有多个字段作用于同一泛型,此时就要按照以下规则:
- 若都为协变,则最终体现为协变
- 若都为逆变,则最终体现为逆变
- 其余所有情况都是不变
案例一:
rust
struct Combo<'a, T> {
a: &'a T,
b: &'a mut T,
}
画个表格分析:
'a |
T |
|
|---|---|---|
&'a |
协变 | 协变 |
&'a mut T |
协变 | 不变 |
Combo<'a, T> |
协变 | 不变 |
- 对于
'a:所有字段都是协变,因此最终就是协变 - 对于
T:由于不全是协变,最终表现为不变
案例二:
rust
struct Combo<'a, T, U, X> {
arg: &'a T,
f1: fn(T) -> U,
f2: fn(X) -> U,
f3: fn(X) -> U,
}
分析:
'a |
T |
U |
X |
|
|---|---|---|---|---|
&'a T |
协变 | 协变 | - | - |
fn(T) -> U |
- | 逆变 | 协变 | - |
fn(X) -> U |
- | - | 协变 | 逆变 |
fn(X) -> U |
- | - | 协变 | 逆变 |
Combo<'a, T, U, X> |
协变 | 不变 | 协变 | 逆变 |
这个案例稍微复杂一点了。
- 对于
T:完全由协变组成,最终表现为协变 - 对于
Y:它不是完全由协变组成,或者完全由逆变组成,最终表现为不变 - 对于
U:完全由协变组成,最终表现为协变 - 对于
X:完全由逆变组成,最终表现为逆变
PhantomData
之前说,内部可变性 加上协变会导致垂悬借用。那么我们只要破坏协变就好了,要如何破坏协变?了解了结构体的型变规则之后,你应该就理解了,给某个内部可变的T加上一个不变或者逆变的字段,就可以破坏协变。
但是假如真的把这个字段添加加进去,一方面它占用了内存空间,另一方面每次创建实例都要给这个字段一个具体的值。但是这个字段实际上根本用不上,我们只希望用它来限制某个泛型参数的型变。
为此Rust提供了一个工具PhantomData,它叫做幻影类型,被包含在std::marker中。它的定义如下:
rust
#[lang = "phantom_data"]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct PhantomData<T: PointeeSized>;
如你所见,这是一个不包含任何字段的类型!因此他也是一个零大小类型,不占用任何内存空间。
它被Rust内部标记,当向<>泛型参数中传入任意类型,Rust都会视为它持有该类型,从而协助借用检查,影响型变。
假设你有一个Combo结构体如下:
rust
struct Combo<'a, T> {
s: &'a T,
}
现在它对'a和T都是协变的,如何让T变成不变?
你可以加一个字段&'a mut T来影响它:
rust
struct Combo<'a, T> {
s: &'a T,
_marker: &'a mut T,
}
这个时候T就变成了不变。但是这种方案并不优雅,刚刚才讲过它的问题。
问题在于,想要影响Combo的T,就必须持有一个&'a mut T。这个时候你就可以使用PhantomData来装作拥有这个类型字段。
rust
struct Combo<'a, T> {
s: &'a T,
_marker: PhantomData<&'a mut T>,
}
使用PhantomData,编译器会认为结构体内部存在一个&'a mut T字段,从而影响到T的型变。但实际上这根本没有造成任何实际上的内存占用,所以得名"幻影类型"。
例如之前的RefCell就使用了PhantomData:
rust
#[stable(feature = "rust1", since = "1.0.0")]
#[must_not_suspend = "holding a RefMut across suspend points can cause BorrowErrors"]
#[rustc_diagnostic_item = "RefCellRefMut"]
pub struct RefMut<'b, T: ?Sized + 'b> {
// NB: we use a pointer instead of `&'b mut T` to avoid `noalias` violations, because a
// `RefMut` argument doesn't hold exclusivity for its whole scope, only until it drops.
value: NonNull<T>,
borrow: BorrowRefMut<'b>,
// `NonNull` is covariant over `T`, so we need to reintroduce invariance.
marker: PhantomData<&'b mut T>,
}
因为这个RefMut可以当做一个可变借用,它在运行时维护。思考一下RefMut和一个真正的借用还有哪些区别?
从借用检查上,RefMut有运行时借用检查,T: ?Sized + 'b的约束也保证了T的内部生命周期长于'b。
从接口上,RefMut实现了Deref,可以自动解引用,基于RAII自动释放借用。
好像这就是可变借用的全部?
不,&'a mut T还有一点,它对'a是协变,对T是不变,而RefMut也要保证这个特性。
T被保存在NonNull<T>中,内部实际存储的是*const T,*const T对于T是实际上是协变!这就违背了可变借用的型变规则。
PhantomData<&'b mut T>告诉编译器,我在逻辑上持有一个可变借用&'b mut T,这样就可以影响到T和'b的型变规则,那么T就变成了一个不变类型。
而对于不可变借用,T是协变的,因此如果去看Ref<T>的结构,就不包含这个PhantomData。