所有权

在 Rust 中,heap 上的一块内存区域是一块 “值”,与之绑定的是一个变量,也就是说变量绑定的,要注意这种绑定关系。 在任何时候,一个值只有一个对应的变量作为所有者

理解了这些概念之后,再来看所有权和它的基本特性:

  1. Rust中的每个值都有一个对应的变量作为它的所有者;
  2. 在同一时间内,只有且仅有一个所有者;
  3. 当所有者离开自己的作用域时,它持有的值就会被释放掉。

所有权的转移

赋值即转移。 如下面的示例,

fn test() {
	let v: Vec<u8> = vec![0;20];
	let u = v 
}

在第二行,u 成为了内存中这个数组数据的所有者,当函数返回时,u 的作用域结束,这块内存随即被释放。

要想让 v 和 u 各自都拥有独立的数据,可以使用 v.clone() 函数,

注意,int, char 等基本类型,在赋值的时候等于自动调用了 clone,所以对于这些基本类型可以放心的像 C/C++ 语言那样使用。

所有权的借用

& 是一个在 C/C++ 和 Golang 中常见的符号,在 Rust 中,用在一个变量上是借用的意思,也就是说所有权不变。

官方文档用这样一个例子来说明借用

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

最后一行的 s 是 s1 的引用,是对数据的借用,s1 仍然是数据的所有者,在 calculate_length() 返回之后,s 也会被销毁,但不影响原始数据。

借用也分可变和不可变,默认是不可变的,向其他变量一样,加上 mut 关键字就成了可变。

需要注意的是,在同一个作用域内,不允许同时有两个可变引用,例如如下代码是错误的

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;  // 错

    println!("{}, {}", r1, r2);
}

还有一点需要注意,同一作用域内,可变引用和不可变引用不能同时出现,如下所示

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

上面的代码错就错在最后一行仍然使用 r1, r2, 也就是说 r1, r2 的作用域与 r3 重叠了,

所以解决方法也很简单,把 print 上移即可,因为 r1, r2 作用域也在 r3之前结束。

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);
}

最后对所有权做一下总结:

  1. 所有的资源只能有一个主人(owner)。
  2. 其它人可以租借这个资源。
    • 同时可以有多个不可变引用(&T)。
    • 同时只可以有一个可变引用(&mut T)。
  3. 但当这个资源被借走时,主人不允许释放或修改该资源。

生命周期

借用方的生命周期不能比出借方(所有者)的生命周期还要长。

函数的生命周期

起初是看到一段奇怪的函数定义,然后才了解到生命周期这个概念

fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
  if x > y {
    &x
  } else {
    &y
  }
}

其中 'a 就是函数的生命周期声明,那么什么时候函数需要带上生命周期呢?

对于一个参数和返回值都包含引用的函数而言,该函数的参数是出借方,函数返回值所绑定到的那个变量就是借用方。所以这种函数也需要满足借用规则(借用方的生命周期不能比出借方的生命周期还要长)。那么就需要对函数返回值的生命周期进行标注,告知编译器函数返回值的生命周期信息。

如果函数的多个输入参数生命周期不同,那么需要标注多个生命周期

fn foo<'X, 'Y, 'Z>(x: &'X str, x: &'Y str, x: &'Z str) -> &'R str {
    ...
}

那么,只有当 R 属于 X, Y, Z 的交集时,Rust 编译器才允许通过。 写成公式就是

Lifetime(R) ⊆ ( Lifetime(X) ∩ Lifetime(Y) ∩ Lifetime(Z) ∩ Lifetime(...) )

回到上面的例子,如果这样写就是非法的

fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    if true { x } else { y }
}

因为编译器无法推导出返回值 a 是否是 b 的子集。 所以需要写成下面这样

fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    if true { x } else { y }
}

看到这里,我有一个疑问: 既然生命周期标注是程序员自己写的,那会不会出现随意标注全部为 a,从而导致形同虚设?

结构体的生命周期

同样,结构体和成员有时要需要明确标注生命周期。

一个包含引用成员的结构体,必须保证结构体本身的生命周期不能超过任何一个引用成员的生命周期。否则就会出现成员已经被销毁之后,结构体还保持对那个成员的引用就会产生悬垂引用。所以这依旧是 rust 的借用规则即借用方(结构体本身)的生命周期不能比出借方(结构体中的引用成员)的生命周期还要长。因此就需要在声明结构体的同时也声明生命周期参数,同时对结构体的引用成员进行生命周期参数标注。

例如

struct Foo<'a> {
  v: &'a i32
}

参考资料