rust语言基础学习: 从几个示例代码理解所有权
2020-07-10
前面学习了Rust的所有权规则、Move和Copy语义、引用和借用的知识。 今天先做一个复习,再从几个代码例子继续加深对Rust所有权的理解。
1.Rust所有权基础知识 #
Rust的所有权主要是用来管理堆内存的,所有权就是堆上数据的拥有和释放权,在Rust中这个权利是独占的,即单一所有权。
1.1 所有权规则 #
Rust有以下3条所有权规则:
- Rust中的每个值都有一个变量,称为它的所有者 (Each value in Rust has a variable that’s called its owner.)
- 一个值在同一时刻有且只有一个所有者 (There can only be one owner at a time.)
- 当所有者离开作用域时,这个值将被丢弃 (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)。
使用借用需要遵循以下借用规则:
- 引用不能超过值的生命周期 (expected lifetime parameter,编译器提示缺少生命周期参数,生命周期是我们后续学习的内容)
- 在同一时刻一个值不能创建多个可变引用(cannot borrow
x
as mutable more than once at a time) - 在同一时刻一个值已经创建了不可变引用,不能再为其创建可变引用(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
,要求我们修改下面的代码,使其可以编译通过:
1fn main() {
2 let vec0 = Vec::new();
3
4 let vec1 = fill_vec(vec0);
5
6 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
7
8 vec1.push(88);
9
10 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
11}
12
13fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
14 let mut vec = vec;
15
16 vec.push(22);
17 vec.push(44);
18 vec.push(66);
19
20 vec
21}
- 第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
就可以了。
1fn main() {
2 let vec0 = Vec::new();
3
4 let mut vec1 = fill_vec(vec0);
5
6 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
7
8 vec1.push(88);
9
10 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
11}
12
13fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
14 let mut vec = vec;
15
16 vec.push(22);
17 vec.push(44);
18 vec.push(66);
19
20 vec
21}
2.2 rustlings中的move_semantics2.rs #
下面的代码时rustlings中的exercises/move_semantics/move_semantics2.rs
,要求在不动第7行的前提下,修改下面的代码,使其可以编译通过:
1fn main() {
2 let vec0 = Vec::new();
3
4 let mut vec1 = fill_vec(vec0);
5
6 // Do not change the following line!
7 println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);
8
9 vec1.push(88);
10
11 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
12}
13
14fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
15 let mut vec = vec;
16
17 vec.push(22);
18 vec.push(44);
19 vec.push(66);
20
21 vec
22}
这段代码第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
,要求是不能增加新的代码行,只是修改现有代码行使其可以编译通过:
1fn main() {
2 let vec0 = Vec::new();
3
4 let mut vec1 = fill_vec(vec0);
5
6 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
7
8 vec1.push(88);
9
10 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
11}
12
13fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
14 vec.push(22);
15 vec.push(44);
16 vec.push(66);
17
18 vec
19}
这段代码也很简单,第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
,使其可以编译通过:
1fn main() {
2 let vec0 = Vec::new();
3
4 let mut vec1 = fill_vec(vec0);
5
6 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
7
8 vec1.push(88);
9
10 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
11}
12
13// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
14fn fill_vec() -> Vec<i32> {
15 let mut vec = vec;
16
17 vec.push(22);
18 vec.push(44);
19 vec.push(66);
20
21 vec
22}
修改完成的代码如下:
1fn main() {
2 let mut vec1 = fill_vec();
3
4 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
5
6 vec1.push(88);
7
8 println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
9}
10
11// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
12fn fill_vec() -> Vec<i32> {
13 let mut vec = Vec::new();
14
15 vec.push(22);
16 vec.push(44);
17 vec.push(66);
18
19 vec
20}
修改完成后,在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
,在不修改、添加、删除代码行的前提下,使其可以编译通过:
1fn main() {
2 let mut x = 100;
3 let y = &mut x;
4 let z = &mut x;
5 *y += 100;
6 *z += 1000;
7 assert_eq!(x, 1200);
8}
这段代码第3行和第4行为变量x创建了两个可变引用y和z,不满足借用规则的第2条"在同一时刻一个值不能创建多个可变引用"。因此得到了编译错误cannot borrow x as mutable more than once at a time
。
修改这个代码的话,只要把第4行和第5行交换一下位置:
1fn main() {
2 let mut x = 100;
3 let y = &mut x;
4 *y += 100;
5 let z = &mut x;
6 *z += 1000;
7 assert_eq!(x, 1200);
8}
这样创建完可变引用后,马上使用了它修改了值的数据,后边的代码可变引用y没有再被用到即y不再活跃,所以也就满足了借用规则。
2.6 分配在堆上的数据能否引用栈上的数据? #
Rust的所有权主要是为了管理堆内存的,那么分配上堆上的数据结构可以引用栈上的数据吗? 这个要看生命周期,只要栈上数据的生命周期大于堆上数据的生命周期就可以,例如下面的代码是可以编译通过的:
1fn main() {
2 let num = 100; // 分配在栈上
3 let vec = vec![&num]; // 分配在堆上
4 println!("{:?}", vec);
5}
但如果栈上数据的生命周期小于堆上数据的生命周期就不可以了,下面的代码无法编译通过,因为不满足借用规则中的"引用不能超过值的生命周期":
1fn main() {
2
3 let mut vec = Vec::new(); // 分配在堆上
4 push(&mut vec);
5 println!("{:?}", vec);
6}
7
8fn push(vec: &mut Vec<&i32>) {
9 let num = 100; // 分配在栈上
10 vec.push(&num); // `num` does not live long enough
11}
2.7 同一个值的可变引用和不可变应用不能共存的例子 #
代码示例如下:
1fn main() {
2 let mut vec1 = vec![1, 2, 3];
3 let vec2 = vec![&vec1[0]];
4
5 for i in 0..100 {
6 vec1.push(i); // cannot borrow `vec1` as mutable because it is also borrowed as immutable
7
8 }
9
10 println!("{:?}", vec2);
11}
编译错误为 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 同一时刻不能创建多个可变引用的例子 #
代码示例如下:
1fn main() {
2 let mut vec1 = vec![1, 2, 3];
3 for elem in vec1.iter_mut() {
4 vec1.push(*elem + 1) // cannot borrow `vec1` as mutable more than once at a time
5 }
6}
编译错误为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同一时刻出现了多个可变引用,这违背了借用规则"在同一时刻一个值不能创建多个可变引用"。