前面学习了Rust的所有权规则、Move和Copy语义、引用和借用的知识。 今天先做一个复习,再从几个代码例子继续加深对Rust所有权的理解。

1.Rust所有权基础知识

Rust的所有权主要是用来管理堆内存的,所有权就是堆上数据的拥有和释放权,在Rust中这个权利是独占的,即单一所有权。

1.1 所有权规则

Rust有以下3条所有权规则:

  1. Rust中的每个值都有一个变量,称为它的所有者 (Each value in Rust has a variable that’s called its owner.)
  2. 一个值在同一时刻有且只有一个所有者 (There can only be one owner at a time.)
  3. 当所有者离开作用域时,这个值将被丢弃 (When the owner goes out of scope, the value will be dropped.)

规则1给出了所有者owner的概念,规则2对应所有权的转移Move语义,规则3对应内存释放Drop。Rust通过单一所有权,将堆上内存管理与分配在栈上的所有者变量生命周期绑定,所有者离开作用域后堆内存也自动释放。

1.2 Move语义和Copy语义

规则2描述的是单一所有权,那么Rust中是如何保证单一所有权的呢?一种方式是,值所有权从变量1转移到变量2,这就是Move语义;另一种方式是将值复制一份,变量1仍拥有旧的值,变量2拥有复制出来的值,这是Copy语义。

Move语义: 变量赋值、函数传参时,如果数据类型未实现Copy trait,会发生所有权转移(Move)

Copy语义: 变量赋值、函数传参时,如果数据类型实现了Copy trait, 就会使用Copy语义,对应的值会被按位拷贝(浅拷贝),产生新的值,不会发生所有权转移

Rust不允许自身或其任何部分实现了Drop trait的类型使用Copy trait

可以从文档https://doc.rust-lang.org/std/marker/trait.Copy.html#implementors查看哪些类型实现了Copy trait中。整理归纳如下:

  • 所有的整形类型,例如i32
  • 布尔类型bool
  • 所有的浮点类型,例如f64
  • 字符类型char
  • 元组,当且仅当其包含的类型也都实现了Copy trait,例如(i32, i32)实现了Copy,但(i32, String)就没有
  • 数组,当且仅当其内部元素类型实现了Copy trait
  • ……

1.3 借用和借用规则

Move语义虽然避免了堆内存上数据被随意引用的问题,但也会带来使用上的不便。 因为发生所有权转移时,失去所有权的变量将会失效,无法被继续使用。

有的时候我们需要使用一下数据,但又不希望发生所有权转移。针对这种情况Rust提供了Borrow语义,当不希望所有权转移时,可以借用数据。

借用就是一个值的所有权在不发生转移的情况下,借给其他变量使用。 在Rust中要使用借用,需要先使用引用语法(&&mut)创建一个引用。注意这里的引用,要同其他编程语言中的引用区分开。在其他编程语言(例如Java)中,一个值的多个引用拥有对值的访问权限,相当于共享了所有权。在Rust中,创建的引用只是拥有临时的使用权,而没有所有权。

在Rust中引用和借用说的是一回事,Rust中将创建一个引用的行为称为借用(borrowing)

使用借用需要遵循以下借用规则:

  1. 引用不能超过值的生命周期 (expected lifetime parameter,编译器提示缺少生命周期参数,生命周期是我们后续学习的内容)
  2. 在同一时刻一个值不能创建多个可变引用(cannot borrow x as mutable more than once at a time)
  3. 在同一时刻一个值已经创建了不可变引用,不能再为其创建可变引用(cannot borrow x as mutable because it is also borrowed as immutable)

2和3可以归并成: 一个值在任意给定的时间,要么只能有一个活跃的可变引用,要么只能有多个不可变引用。

2.从代码示例理解

下面从几个代码示例理解前面已经整理和复习的知识点。

2.1 rustlings中的move_semantics1.rs

下面的代码时rustlings中的exercises/move_semantics/move_semantics1.rs,要求我们修改下面的代码,使其可以编译通过:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let vec0 = Vec::new();

    let vec1 = fill_vec(vec0);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    let mut vec = vec;

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}
  • 第2行,创建一个Vec,将分配在堆上,vec0变量获得所有权
  • 第4行调用fill_vec函数后,所有权从vec0转移到了fill_vec函数的vec参数上。
  • 第14行,因为Rust中允许变量屏蔽(variable shadowning),声明的vec变量屏蔽了参数vec,所有权转移到了函数局部变量vec
  • 第20行,vec被返回到第4行函数调用结果使用vec1变量接收,所有权转移到了vec

上面分析了这段代码从所有权转移角度上,满足所有权规则。问题出在了对变量的使用上,因为rust中使用let声明变量默认是不可修改,而这段代码第8行使用了不可变变量vec1向Vec添加元素。 为了使这段代码编译通过,只要将变量vec1声明为可变mut就可以了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let vec0 = Vec::new();

    let mut vec1 = fill_vec(vec0);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    let mut vec = vec;

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

2.2 rustlings中的move_semantics2.rs

下面的代码时rustlings中的exercises/move_semantics/move_semantics2.rs,要求在不动第7行的前提下,修改下面的代码,使其可以编译通过:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
    let vec0 = Vec::new();

    let mut vec1 = fill_vec(vec0);

    // Do not change the following line!
    println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    let mut vec = vec;

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

这段代码第4行调用fill_vec函数后,所有权从vec0转移到函数内的vec参数上,vec0将失效,所以第7行还使用vec0的话,将会得到value borrowed here after move编译错误。

单从这道题的要求,只要不动第7行,保证编译通过就行。结合前面学过的所有权知识,后边还要用到vec0,所以不希望发生所有权转移,最简单的修改方式是将调用fill_vec(vec0)方式修改成fill_vec(vec0.clone())。 即手动复制一份数据,这样不会发生所有权转移。当然,实际开发的话手动clone效率低不推荐,这里只是为了应试~

2.3 rustlings中的move_semantics3.rs

下面的代码时rustlings中的exercises/move_semantics/move_semantics3.rs,要求是不能增加新的代码行,只是修改现有代码行使其可以编译通过:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
    let vec0 = Vec::new();

    let mut vec1 = fill_vec(vec0);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

这段代码也很简单,第4行vec0的所有权被转移后它就失效了,后边也没有再使用vec0,所以这块没有问题。问题出现fill_vec函数内部,因为在内部要修改Vec中的数据,所以需要将参数vec声明成可变mut的。 修改方法就是把fill_vec函数的签名修改成fn fill_vec(mut vec: Vec<i32>) -> Vec<i32>即可。

2.4 rustlings中的move_semantics4.rs

下面的代码时rustlings中的exercises/move_semantics/move_semantics4.rs,要求重构代码去掉main函数中的vec0,使其可以编译通过:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
    let vec0 = Vec::new();

    let mut vec1 = fill_vec(vec0);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
fn fill_vec() -> Vec<i32> {
    let mut vec = vec;

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

修改完成的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
    let mut vec1 = fill_vec();

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
fn fill_vec() -> Vec<i32> {
    let mut vec = Vec::new();

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

修改完成后,在fill_vec函数内部,创建了一个Vec,分配在堆上,其被变量vec所有,往Vec中添加了3个元素后,函数fill_vec返回。Vec的所有权从fill_vec函数内部转移到了main函数中的vec1变量。

2.5 rustlings中的move_semantics5.rs

下面的代码时rustlings中的exercises/move_semantics/move_semantics5.rs,在不修改、添加、删除代码行的前提下,使其可以编译通过:

1
2
3
4
5
6
7
8
fn main() {
    let mut x = 100;
    let y = &mut x;
    let z = &mut x;
    *y += 100;
    *z += 1000;
    assert_eq!(x, 1200);
}

这段代码第3行和第4行为变量x创建了两个可变引用y和z,不满足借用规则的第2条"在同一时刻一个值不能创建多个可变引用"。因此得到了编译错误cannot borrow x as mutable more than once at a time

修改这个代码的话,只要把第4行和第5行交换一下位置:

1
2
3
4
5
6
7
8
fn main() {
    let mut x = 100;
    let y = &mut x;
    *y += 100;
    let z = &mut x;
    *z += 1000;
    assert_eq!(x, 1200);
}

这样创建完可变引用后,马上使用了它修改了值的数据,后边的代码可变引用y没有再被用到即y不再活跃,所以也就满足了借用规则。

2.6 分配在堆上的数据能否引用栈上的数据?

Rust的所有权主要是为了管理堆内存的,那么分配上堆上的数据结构可以引用栈上的数据吗? 这个要看生命周期,只要栈上数据的生命周期大于堆上数据的生命周期就可以,例如下面的代码是可以编译通过的:

1
2
3
4
5
fn main() {
    let num = 100; // 分配在栈上
    let vec = vec![&num]; // 分配在堆上
    println!("{:?}", vec);
}

但如果栈上数据的生命周期小于堆上数据的生命周期就不可以了,下面的代码无法编译通过,因为不满足借用规则中的"引用不能超过值的生命周期":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {

    let mut vec = Vec::new(); // 分配在堆上
    push(&mut vec);
    println!("{:?}", vec);
}

fn push(vec: &mut Vec<&i32>) {
    let num = 100; // 分配在栈上
    vec.push(&num); // `num` does not live long enough
}

2.7 同一个值的可变引用和不可变应用不能共存的例子

代码示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let mut vec1 = vec![1, 2, 3];
    let vec2 = vec![&vec1[0]];

    for i in 0..100 {
        vec1.push(i); // cannot borrow `vec1` as mutable because it is also borrowed as immutable

    }

    println!("{:?}", vec2);
}

编译错误为 cannot borrow vec1 as mutable because it is also borrowed as immutable

在这段代码中,vec2中借用了vec1中的第一个元素,创建了一个不可变引用&vec![0]。在后边的循环中调用vec1.push(i);时,注意push方法的签名为pub fn push(&mut self, value: T),即在push时又使用了vec1的可变引用。 这违背了借用规则"在同一时刻一个值已经创建了不可变引用,不能再为其创建可变引用"。

2.8 同一时刻不能创建多个可变引用的例子

代码示例如下:

1
2
3
4
5
6
fn main() {
    let mut vec1 = vec![1, 2, 3];
    for elem in vec1.iter_mut() {
        vec1.push(*elem + 1) // cannot borrow `vec1` as mutable more than once at a time
    }   
}

编译错误为cannot borrow vec1 as mutable more than once at a time。 这段代码中,vec1.iter_mut()返回一个迭代器用于修改Vec中的值,elem&mut i32可变引用,再调用vec1.push(*elem + 1)时,注意push方法的签名为pub fn push(&mut self, value: T),即在push时又使用了vec1的可变引用。 对Vec同一时刻出现了多个可变引用,这违背了借用规则"在同一时刻一个值不能创建多个可变引用"。

参考