解引用的所有权转移问题

前面对Move、Copy和所有权相关的内容做了详细的解释,相信变量赋值、函数传参时的所有权问题应该不再难理解。

但是,所有权的转移并不仅仅只发生在这两种相对比较明显的情况下。例如,解引用操作有时候也需要转移所有权。

事实上,所有需要使用值的地方,且这个值来自于对某个变量的操作时,都可能发生所有权转移:通过对某个变量的操作,将其值转移或拷贝出来,以便执行下一步操作。

显然上述解释很难理解,因此这里举个例子。如下代码所示:


#![allow(unused)]
fn main() {
let x = &y;
if *x < 33 {}
}

假如y是某个具体值(比如数值100),x是它的引用,当评估计算表达式*x < 33时,必须获取到*x的值才能和值33进行比较。也就是说,小于号左右两边是需要值的地方,右边已经提供了具体的值33,而左边的值来自于对变量x的解引用操作*x,所以*x必然需要将其存储的值转移或拷贝出来,才能继续执行下一步的比较操作。

此外,赋值操作符的右边也是需要值的地方:


#![allow(unused)]
fn main() {
let x = 3;   // 右边提供了值3
let y = &x;  // 右边提供了值:引用值&x
             // &x引用也需要值:值由x提供
let z = *y;  // 右边提供了值:解引用y得到的值
}

那么,哪些地方需要值呢?

在官方手册The Rust Reference: Expressions中,将表达式分为两类:位置表达式(place expression)和值表达式(value expression)。位置表达式用于描述内存位置,比如变量、引用等;值表达式用于描述实际的数据,除位置表达式外的都是值表达式。所有的位置表达式都需要结合值表达式,例如变量(位置表达式)需要绑定一个值,引用则需要对一个值进行引用。

因此,除了上述官方手册中列出的表示位置表达式的几种情况外,其余涉及数据的地方,都需要值。一般来说,根据代码上下文自行分析哪里需要值,这并非难事。不过,有些情况下并不那么明显。

下面通过一些比较典型的示例来分析解引用操作涉及到的所有权转移问题。

理解Rust的解引用操作

对于如下代码:


#![allow(unused)]
fn main() {
let a = &"junmajinlong.com".to_string();
// let b = *a;         // (1).取消注释将报错
let c = (*a).clone();  // (2).正确
let d = &*a;           // (3).正确

let x = &3;
let y = *x;      // (4).正确
}

上面的变量a是String字符串的引用。如果取消代码行(1)的注释,将编译错误,代码行(2)(3)(4)则是正确不报错的。为什么let b = *a;这个看似简单且正确的语句会报错呢?

实际上,解引用操作符*的作用并不是直接去获取其指向的值,解引用操作符*的行为类似于导航,只是导航到数据处,导航之后,根据上下文来决定如何处理该数据:取走(Move)或拷贝(Copy)或其他处理方式。

根据前文结论,变量b、c、d均需要绑定一个值,这个值来自于赋值操作符右边的计算结果,这三个赋值操作符的右边都涉及到对变量a的解引用操作*a,但不同处理方式会导致不同行为结果。

代码行(1)let b = *a;的上下文是b需要绑定一个值,这个值来自于a指向的数据,因此,通过解引用操作*a导航到数据后,需要消费该数据将其绑定到变量b上。也就是需要Move或Copy该数据给变量b。由于String类型没有实现Copy Trait,因此只能Move,但这里却不允许Move,因为Move后会使得引用类型的变量a变成悬垂指针。因此整个代码行(1)是错误的代码。而i32类型实现了Copy,因此代码行(4)是正确的代码。

从另一个角度来看待代码行(1)也许更好理解,变量b需要绑定一个来自于变量a的值,显然这个值要么从变量a处Move,要么从变量a处Copy。而String类型没有实现Copy,因此只能Move。

代码行(2)let c = (*a).clone();的上下文是c需要绑定一个值,这个值来自于a所指向数据的克隆,因此通过解引用操作*a导航到数据后,无需消费该数据,而是克隆一份数据后绑定到变量c上,原数据仍然保留。因此,代码行(2)是正确的代码。

代码行(3)let d = &*a;和代码行(2)是类似的,其上下文是d需要绑定一个值,这个值是对a所指向数据的引用,因此通过解引用操作*a导航到数据后,无需消费该数据,直接创建一个指向该数据的引用绑定到变量d上,原数据仍然保留在内存中。因此,代码行(3)是正确的代码。

可以通过一个类比来帮助理解这里的行为。假如A给了B一张藏宝图(引用看作是藏宝图),要求B去藏宝地取一把宝剑,B根据藏宝图找到藏宝地(即解引用操作)。当B取走宝剑交给A后(即Move),这张藏宝图将变成一张没用任何价值的地图(即,引用变成悬垂指针),但如果B发现藏宝地的这把宝剑可以无限复制(即实现了Copy),B会复制一把宝剑交给A,这时藏宝图仍然是有价值的,因为B会留着藏宝图以备后用。另外,如果B发现藏宝地除了宝剑之外,还有其他宝物,B也会留着藏宝图以备后用(即对数据的其他处理方式,如clone())。

举一反三

注:本节涉及Struct、Enum相关内容,如不了解可先跳过,可了解Struct、Enum后再回来阅读。

假如定义了一个Enum类型的Vip:


#![allow(unused)]
fn main() {
enum Vip {
  Vip0 = 0,
  Vip1,
  Vip2,
  Vip3,
}
}

下面为Vip类型定义如下方法is_vip时,将报错:


#![allow(unused)]
fn main() {
impl Vip {
  fn is_vip(&self) -> bool {
    // 下面的*self会导致错误
    if (*self as u8) < 1 {
      return false;
    }
    true
  }
}
}

分析一下这里报错的原因。当调用is_vip()时,传递给self参数的是Vip实例对象的引用,这里想要通过*self来解引用从而获取其指向的数据,然后将其转换为u8类型。

根据前文的解释,*self as u8需要*self提供一个值才能进一步将其转换为u8类型,这个值要么来自于对self所指向数据的Move操作,要么来自于对self所指向数据的Copy操作。Vip没有实现Copy,因此只能Move,但Move之后,会让self变成悬垂指针。因此,代码是错误的。

再看一个类似的Struct类型的示例。


#![allow(unused)]
fn main() {
struct P {
  vip: u8,
}

impl P {
  fn is_vip(&self) -> bool {
    // 不会因为*self而报错
    if (*self).vip < 1 {
      return false;
    }
    true
  }
}
}

上面的代码是正确的,因为(*self).vip < 1中,比较操作符的左边需要一个值,值来自于对self所指向数据的vip字段,这里的解引用操作*self导航到self所指向数据后,并不会直接消费整个Struct的实例对象,而是从实例对象中取vip字段的数据,vip字段是u8类型,实现了Copy Trait,因此取vip字段数据时是Copy而不是Move,这不会导致引用类型的self变量失效。因此,上述代码可正确编译。

但如果vip字段不是u8类型而是String类型,则一样会报错,因为String类型没有实现Copy。


#![allow(unused)]
fn main() {
struct P {
  vip: String,
}

impl P {
  fn is_vip(&self) -> bool {
    // *self报错
    if (*self).vip < "xyz".to_string() {
      return false;
    }
    true
  }
}
}