以下内容为本人学习过程中的记录笔记,其中可能存在不准确或错误,欢迎勘误及指正

引言

在Rust的基于所有权的内存管理模式中,对于引用类型变量,其所有权会在传递过程中发生变化。比如下面的代码中,当我们将String类型的变量传入函数后,s的所有权发生变更,我们想在main函数中继续使用s就不行了

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s = "hello".to_string();
// 函数get_ownership()获得了变量s的所有权
get_ownership(s);
println!("{}", s);
}

fn get_ownership(s: String) {
println!("{}", s);
// get_ownership()函数执行完,s被释放
}

但是在实际开发过程中我们常常需要继续在main函数中使用这些变量,这时主要三种方法可以满足类似的需求

深拷贝

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = "hello".to_string();
// 将原变量深拷贝后传递给调用函数
get_ownership(s.clone());
println!("{}", s);
}

fn get_ownership(sc: String) {
println!("{}", sc);
}

通过clone(),确实可以解决后续在main函数中使用变量的问题,但在实际情况中,我们如果要对原来的变量进行处理而不是单纯的传递,采用clone()就不是很友好了

归还所有权

归还所有权就是当调用函数结束的时候,将变量的所有权返回至原函数,见下面的例子

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = "hello".to_string();
let s = give_back(s);
println!("{}", s);
}

fn give_back(s: String) -> String {
println!("{}", s);
s // 通过返回值,将s的所有权返回原函数
}

归还所有权很好理解,但这样写出的代码比较冗余,且在返回值的处理上会比较麻烦

传递引用

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = "hello".to_string();
// 通过传递引用,将s的所有权保留在main函数中
give_reference(&s);
println!("{}", s);
}

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

通过传递引用不会改变原变量的所有权,这样就可以避免后续处理所有权的麻烦

引用(Reference)

我们先分析下以下例子中的变量s和&s在内存中的关系。通过示意图可以看出:在引用变量s时程序创建了一个指针&s,它指向了变量s中指向堆内存数据的指针,当我们将&s传递到外部函数时,s1获得&s的所有权,在s1(&s)被释放后,不会导致变量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
fn main() {
let s = "hello".to_string();
let length = calc_len(&s);
println!("{}", s);
}

fn calc_len(s1: &String) -> usize {
s1.len()
}

// 引用在内存上的相互关系
/*
&s s
+----+-----+ +--------+-----+ +-------+-----+
|name|value| | name |value| | index |value|
+----+-----+ +--------+-----+ +-------+-----+
| ptr|------------->| ptr |------------->| 0 | h |
+----+-----+ +--------+-----+ +-------+-----+
| len | 5 | | 1 | e |
+--------+-----+ +-------+-----+
|capacity| 5 | | 2 | l |
+--------+-----+ +-------+-----+
| 3 | l |
+-------+-----+
| 4 | o |
+-------+-----+
*/

Rust中的&符号就表示引用,它允许调用值而不取得其所有权

借用(Borrow)

在Rust中将创建一个引用的行为称为”借用”,即将引用作为函数参数的行为就是”借用”。如同现实生活中,我们向别人借物品,使用完毕后必须还回去一样,因为我们没有该物品的所有权

可变引用

采用类似&String写法时,默认情况下我们是没法改变引用值的,但在很多场景中又确有修改引用值的需求。在Rust中提供了可变引用实现此类功能,具体见下面的例子

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = "hello".to_string();
add_str(&mut s);
println!("{}", s);
}

fn add_str(s: &mut String) {
// 通过可变引用修改s中的数据
s.push_str(", world!");
}

可变引用的两个限制

Rust虽然提供了可变引用的方法,但同时也做了一些限制以保证内存安全

限制一

在特定作用域内,对某一块数据只能有一个可变引用,以避免产生数据竞争 。比如下面的代码在编译时就会报错
1
2
3
4
5
6
fn main() {
let mut s = "hello".to_string();
let s1 = &mut s;
let s2 = &mut s;
println!("s1: {} s2: {}", s1, s2);
}

可能发生数据竞争的三种行为:

  1. 多个指针同时访问同一个数据
  2. 至少有一个指针用于写入数据
  3. 没有使用任何机制同步对数据的访问行为
对于以上的限制,我们其实可以通过采用划分作用域的方法达到非同时创建多个可变引用的目的 ,比如以下的例子
1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = "hello".to_string();
{
let s1 = &mut s;
s1.push_str(",world");
}
let s2 = &mut s;
s2.push_str("!");
println!("{}", s);
}

限制二

对一个数据可以同时存在多个不可变引用,但不能同时存在可变引用与不可变引用 ,因为这会导致不可变引用的作用失效
1
2
3
4
5
6
7
8
fn main() {
let mut s = "hello".to_string();
let s1 = &s;
// 此处会发生错误,因为同时存在对变量s的不可变引用和可变引用
let s2 =&mut s;

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