详细解释Rust的所有权和borrow规则

Rust的所有权系统使得Rust无需GC即可保证内存安全,它影响整个Rust,也使得Rust的很多编码方式和其他语言不太一样。

Rust的栈和堆

栈中的所有数据都必须占用已知且固定的大小,而那些在编译时大小未知或大小可能变化的数据,需存储在堆上。

那些大小固定的基本数据类型(int、float、bool、char、tuple、array)都存储在栈上,此外,指针的大小是已知且固定的,因此指针也存储在栈上。

堆是缺乏组织的:向堆放入数据时,要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针(pointer)。这个过程称作在堆上分配内存(allocating on the heap),有时简称为分配(allocating)。将数据推入栈中并不被认为是分配。

入栈比在堆上分配内存要快,因为入栈时操作系统无需为存储新数据去搜索内存空间,其位置总是在栈顶。在堆上分配内存则需要更多的工作,因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备,因此在堆上分配大量的空间会比较消耗时间。

访问堆上的数据也比访问栈上的数据慢,因为必须通过栈上的指针来访问其指向的堆内数据,这比访问栈中数据多一次内存跳转。

当调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中,当函数结束时,这些值被移出栈。

Rust的所有权系统用于管理堆数据。例如,String::from("hello")创建的String类型的字符串就是保存在堆中的,在创建这个字符串时,需要请求操作系统分配堆内存,然后返回这个堆内存的地址,当不再使用这个字符串时,也就是回收这个字符串时,则会用到所有权系统。

变量作用域

Rust的所有权系统和作用域息息相关。

在Rust中,可以单独使用一个大括号来开启一个作用域:

1
2
3
4
{                      // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s是有效的
// 使用 s
} // 此作用域已结束,s 不再有效

上面的代码中,变量s绑定了字符串字面值,在跳出作用域后,变量s和内存中的字符串字面值都被销毁。

Rust所有权规则概述

所有权规则如下:

  • Rust中的每个值都有一个被称为其所有者(owner)的变量(注:值的所有者是某个变量)
  • 值在任一时刻有且只有一个所有者
  • 当所有者(变量)离开作用域,这个值将被丢弃
    • 当所有者离开作用域时,将调用一个特殊的名为drop的函数,该函数内部用于放置释放内存的代码

例如:

1
2
3
4
5
fn main(){
{
let mut s = String::from("hello"); // 堆中字符串的owner是s
} // 跳出作用域,s离开作用域,s被销毁,堆中的字符串也被回收
}

Rust中变量的移动

对于如下代码:

1
2
3
4
5
6
7
fn main(){
// s1绑定了栈中的3(因为3是大小固定的整型数据)
let s1 = 3;
let s2 = s1; // 拷贝s1的值给s2,现在栈中有两个3

println!("{},{}", s1, s2);
}

变量s1绑定了栈中的数据3,然后拷贝了s1并赋值给s2,因为s1绑定的是栈中数据,所以拷贝s1其实是拷贝了栈中的3,使得栈中有两个3。

再看下面堆中数据的赋值。

1
2
3
4
5
6
7
fn main(){
let s1 = String::from("hello");
let s2 = s1;

// 将报错error: borrow of moved value: `s1`
println!("{},{}", s1, s2);
}

变量s1绑定的是堆中数据(String类型的hello字符串),此时该数据的所有者是s1。

当执行let s2 = s1时,将不会拷贝堆中数据赋值给s2,也不会像其他语言一样让变量s2也绑定堆中数据。

因此,下图的内存引用方式不适用于Rust(注:String类型字符串存储在堆中,但其变量s1存储在栈中,s1是一个胖指针结构,该结构包含一个指向堆中数据的指针,一个表示数据当前大小的长度,以及一个表示操作系统为该数据所分配的内存空间大小)。

如果Rust采用这种内存引用方式,按照Rust的所有权规则,变量在跳出作用域后就销毁堆中对应数据,那么在s1和s2离开作用域时会导致二次释放同一段堆内存,这会导致内存污染。

于是Rust采用更直接的方式,当执行let s2 = s1时,直接让s1无效,而是只让s2绑定堆内存的数据。也就是将s1移动到s2,也称为堆数据的所有权从s1移给s2

所有权移动后修改数据

定义变量的时候,加上mut表示变量可修改。

当发生所有权转移时,后来拥有所有权的变量也可以加上mut,使得可以通过该变量修改数据。

1
2
3
4
5
let mut x = String::from("hello");
let y = x; // x将所有权转移给y,但y无法修改字符串

let a = String::from("hello");
let mut b = a; // 虽然a无法修改字符串,但转移所有权后,b可修改字符串

Rust堆内存数据的拷贝

虽然Rust绝不会自动拷贝堆内存中的数据,但可以使用clone()方法手动拷贝它们。

1
2
3
4
5
fn main(){
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{},{}", s1, s2);
}

要注意,如果拷贝的堆内存数据较大,这会严重影响性能。但如果要拷贝的堆数据不大,可以不用顾虑拷贝堆内存带来的效率问题。

Rust栈中的数据拷贝

对于前面的示例:

1
2
3
4
5
fn main(){
let s1 = 3;
let s2 = s1;
println!("{},{}", s1, s2);
}

上面的代码不会报错,因为3是整型数据,它保存在栈中而不是堆中。因为栈中数据大小是固定的,拷贝起来速度很快,所以没有必要在拷贝时让s1无效。

也就是说,对于栈中数据,调用clone()方法和直接拷贝变量的效果是一致的。

实际上,上面s1之所以仍然有效,是因为整型数据拥有Copy trait。

当某类型拥有Copy trait时,将旧的变量赋值给新变量后仍然可用。

那么什么类型是Copy的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是Copy的,不需要分配内存或某种形式资源的类型是Copy的。如下是一些Copy的类型:

  • 所有整数类型,比如u32
  • 所有浮点数类型,比如f64
  • 布尔类型,bool,它的值是true和false
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是Copy的时候。比如(i32, i32)是Copy的,但(i32, String)不是

函数参数和返回值的所有权移动

函数参数类似于变量赋值,在调用函数时,会将所有权移动给函数参数。

函数返回时,返回值的所有权从函数内移动到函数外变量。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main(){
let s1 = String::from("hello");

// 所有权从s1移动到f1的参数
// 然后f1返回值的所有权移动给s2
let s2 = f1(s1);
println!("{}", s2); // 注意,println!()不会转移参数所有权

let x = 4; // x绑定的是栈中数据(是Copy的)
f2(x); // 没有移动所有权,而是拷贝一份给f2参数
} // 首先x跳出作用域,其对应栈数据出栈,
// 然后s2跳出作用域,并释放对应堆内存数据,
// 最后s1跳出作用域,s1没有所有权,所以没有任何其他影响

fn f1(s: String) -> String {
let ss = String::from("world");
println!("{},{}", s,ss);
s // s绑定的堆数据所有权移动到函数外
} // ss跳出作用域,其对应堆中数据被释放

fn f2(i: i32){
println!("{}",i);
} // i跳出作用域,i对应栈数据出栈

很多时候,变量传参之后丢失所有权是非常不方便的,这意味着函数调用之后变量就不可用了。为了解决这个问题,可以将变量的引用传递给参数,这样不会丢失所有权。

引用和所有权借用

所有权不仅可以转移(原变量会丢失所有权),还可以借用所有权(borrow ownership)。

使用变量var的引用,即可借用变量var的所有权,借完之后会自动交还所有权给var,从而使得原变量var不丢失所有权。

例如:

1
2
3
4
5
6
7
8
9
fn main(){
{
let s = String::from("hello");
let sf1 = &s;
let sf2 = &s;
println!("{},{},{}",s,sf1,sf2); // 实际上这里也借用了s的所有权
// 使用完成后,这里就开始交还所有权
} // sf2离开,sf1离开,s离开
}

也可以将变量的引用传递给函数的参数,从而保证在调用函数时变量不会丢失所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main(){
let s = String::from("hello");
let s1 = s.clone();

f1(s1); // s1丢失所有权,s1将因此而效
// println!("{}", s1);

let l = f2(&s); // 传递s的(不可变)引用,借用s所有权
// 交还s所有权
println!("{} size: {}", s, l); // s仍然可用
}

fn f1(s: String){
println!("{}", s);
}

fn f2(s: &String)->usize{
s.len() // len()返回值类型是usize
}

可变引用和不可变引用的所有权规则

变量的引用分为可变引用&mut var和不可变引用&var

  • 不可变引用:相当于只读引用(借用只读权),不允许修改其引用变量的堆数据
  • 可变引用:相当于可写引用(借用可写权),允许修改其引用变量的堆数据
  • 多个不可变引用可共存(可同时读)
  • 可变引用具有排他性,不允许多个可变引用共存,也不允许可变引用和不可变引用共存(写时不允许读也不允许其他写)
  • 得益于可变引用的排他性,直接避免了可能存在的同时修改数据导致的数据不同步问题,也避免了读取过期数据的问题

前面示例中f2(&s)传递给函数参数的是变量s的不可变引用&s,即借用了s的只读权,因此无法在函数内部修改该引用的堆数据值。

如要允许通过变量var的引用去修改堆数据值,要求:

  • var是可变的,即let mut var = xxx
  • var的引用,要求引用可写的var,即let varf = &mut var

例如:

1
2
3
4
5
6
7
8
9
10
fn main(){
let mut s = String::from("hello");
f1(&mut s); // 将s的可写权借给函数f1的参数
// f1退出后,交还所有权给s
println!("{}", s);
}

fn f1(s: &mut String){
s.push_str("world");
}

使用可变引用时,一定要注意它的排他性:可变引用和可变引用不可共存,可变引用和不可变引用也不可共存。

例如:

1
2
3
4
5
6
7
8
fn main(){
let mut s = String::from("hello");
let sf = &mut s; // 可变引用,可通过sf修改堆数据
sf.push_str("world");

// 下面报错
// let sf1 = &s; // 可变引用和不可变引用不可共存
}

另外,创建可变引用后,在使用可变引用之前不允许使用原变量(不能访问也不能修改原变量)。本示例之后将给出更严谨的说法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fn main() {
// 正确
let mut s = String::from("hello");
let sf = &mut s;
sf.push_str("world");
println!("{}", sf); // 这里先使用了可变引用sf,后面可以继续使用原变量s
println!("{}", s);

// 错误
let mut ss = String::from("hello");
let ssf = &mut ss;
ssf.push_str("world");
ss.push_str("world"); // 后面代码使用了可变引用,这里不能使用ss
println!("{}", ss); // 后面代码使用了可变引用,这里不能使用ss
println!("{}", ssf); // 这里使用了可变引用,前面代码就不能使用原变量ss
// 删除本行,前面两行使用ss的代码可编译成功

// 这也是错的
let mut sss = String::from("hello");
let sssf = &mut sss;
sssf.push_str("world");
println!("{},{}", sss,sssf); // 不允许同时使用原始变量和不可变引用

// 这也是错的
let mut ssss = String::from("hello");
ssss.push_str("HELLO"); // 存在可变引用之前,可使用原始变量
let ssssf = &mut ssss;
ssssf.push_str("world");
ssss.push_str("WORLD"); // 不允许在使用可变引用前使用(访问或修改)原始变量
println!("{}", ssssf); // 删除本行,上一行将编译通过
}

所有权的作用域

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut s = String::from("hello"); //<---+
// |
let sf = &s; //<------------+ |
// (2) |
println!("{},{}",s,sf);//<--+ |
// (1)
let ssf = &mut s;//<--------+ |
// (3) |
println!("{}", ssf);//<-----+ |
// |
println!("{}", s);//<--------------------+
}

原变量s的所有权作用域是(1),只读引用sf的所有权作用域是(2),可写引用ssf的所有权作用域是(3)。

可写引用的所有权作用域(3)内,不能使用原变量和其他引用。因此,(3)的内部不允许使用sf和s。

容器集合类型的所有权规则

前面所介绍的都是标量类型的所有权规则,此处再简单解释一下容器类型(比如tuple/array/struct/enum等)的所有权。

容器类型中可能包含栈中数据值(实现了Copy的),也可能包含堆中数据值(未实现Copy的)。例如:

1
let tup = (5, String::from("hello"));

容器变量拥有容器中所有元素值的所有权

因此,当上面tup的第二个元素的所有权被借走后,tup将不再拥有它的所有权,这个元素将不可使用,tup自身也不可使用,但仍然可以使用tup的第一个元素。

1
2
3
4
5
6
7
8
#[derive(Debug)]

let tup = (5, String::from("hello"));
let (x, y) = tup; // 5拷贝后赋值给x,堆中字符串则borrow给y
println!("{},{}", x, y); // 正确
println!("{}", tup.0); // 正确
println!("{}", tup.1); // 错误
println!("{:?}", tup); // 错误

如果想要让原始容器变量继续可用,要么丢弃那些没有实现Copy的堆中数据,要么clone()拷贝堆中数据后再borrow。

1
2
3
4
5
6
7
// 方式一
let (x, _) = tup;
println!("{}", tup.1); // 正确

// 方式二
let (x, y) = tup.clone();
println!("{}", tup.1); // 正确

但需要注意容器内元素的可变引用和不可变引用:容器和容器内元素的引用是相互独立的。当容器内元素已经存在可变引用,仍然能够borrow容器自身的可变或不可变引用,这是因为容器和容器内元素在内存中的地址不同。

但是,Rust不允许任何可能的数据竞争和数据不一致性问题发生。因此,即使容器和容器元素的引用是独立的,但要求它们满足所有权的作用域规则

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Struct Person {
name: String,
gender: String,
age: u32,
}

fn main(){
let mut p = Person {
name: String::from("junmajinlong"),
gender: "male".to_string(),
age: 23,
};

// 先borrow结构体p的gender可变所有权
// 再borrow结构体自身的可变所有权
let g = &mut p.gender;
let pp = &mut p; // 不会报错

// 但要满足所有权的作用域规则
println!("{:?}", p); // 本行报错,因为g的所有权在下一行才结束
println!("{}", g;
}

悬垂引用

在支持指针操作的语言中,一不小心就会因为释放内存而导致指向该数据的指针变成悬垂指针(dangling pointer)。

Rust的编译器保证永远不会出现悬垂引用:引用必须总是有效。即引用必须在释放数据之前丢弃,而不能先释放数据的内存而继续持有引用。

例如,下面的代码不会通过编译:

1
2
3
4
5
6
7
8
fn main(){
let sf = f(); // sf是一个无效引用
}

fn f() -> &String {
let s = String::from("hello");
&s // 返回s的引用
} // s跳出作用域,堆中String字符串被释放

编译时将报错:

1
2
3
4
5
6
7
error[E0106]: missing lifetime specifier
--> src\main.rs:6:11
|
6 | fn f() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

因此,可让函数直接返回String而非它的引用。

1
2
3
4
5
6
7
8
fn main(){
let sf = f(); // sf是一个无效引用
}

fn f() -> String {
let s = String::from("hello");
s // 返回s的引用
} // s跳出作用域,堆中String字符串被释放