所有权

介绍

Rust 使用所有权机制来管理内存,实际上是一组规则。这会与其他语言不同,一些语言使用垃圾回收机制来管理内存,如 Java 以及运行在 JVM 下的其他语言;还有一些需要手动释放内存,例如 C++等。与这些语言都不同,Rust 使用自己新的一套机制(所有权)来管理内存,编译时,会检查是否通过这些规则,而运行时并不会影响运行效率。

所有权规则

  1. Rust 中的每一个值都有一个 所有者(owner)。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

变量的作用域

根据所有权的规则,当一个值离开作用域就无效。例如,使用官网里的例子

1
2
3
4
5
6
7
8
let s = "hello1";

{ // s 在这里无效,它尚未声明
let s = "hello2"; // 从此处起,s 是有效的

// 使用 s
} // 此作用域已结束,s 不再有效

hello2在进入代码块中的作用域时生效,掩盖原来的hello1,在离开代码块后,失效。

总之:

  • 当 s 进入作用域 时,它就是有效的。
  • 这一直持续到它 离开作用域 为止。

而 Rust 的作用域范围与其他语言一样。

String 类型介绍

Rust 的 String 类型不可变,这点和 Java 类似。它也可以存储编译时未知的数据,例如用户输入等。

创建

可以使用from方法创建

1
let s = String::from("hello");

也可以这样

1
2
3
let mut s = String::from("hello");
s.push_str(", world!");
println!("{s}");

可是为什么 string 是不可变的,却在第二种情况下可以“改变”内容(String可变,字面值不可变)呢?

内存分配

要支持String可变,及维护一个内存长度未知的内存空间,那就一定要在堆上分配一块在编译时未知大小的内存空间。

也就是说

  • 必须在运行时向内存分配器(memory allocator) 请求内存
  • 需要处理完String时将内存返回给分配器的方法。

第一种方式创建,每种语言的实现方式都差不多,即String的实现(implementation)请求所需要的内存(编译时就知道了)。

第二种方式就不太一样了:

  1. 有 GC 的语言中,GC 会识别并清理不再使用的内存;
  2. 没有 GC 的语言中,我们需要手动释放内存
  3. Rust 与前两者都不同,而是采取了离开作用域就释放的机制(当变量离开作用域时,Rust 会自动调用一个特殊函数drop)

变量与数据交互的方式(一):移动

有下述例子:

1
2
let x = 5;
let y = x;

这段代码做了以下的事情,将5绑定到 x;拷贝x的值并绑定到y。因为5这个值是固定的值,所以这两个变量就直接存到栈中。

第二个例子:

1
2
let s1 = String::from("hello");
let s2 = s1;

这段代码与上段代码不太一样,这里s2并不是拷贝并绑定的操作。

我们关注String内部结构,左侧存在栈上,右侧存在堆上。
Alt text

当我们将s1赋值给s2,String 上的内容会被复制,意味着我们拷贝了它栈上的内容,但并没有拷贝堆上的内容。
Alt text

这个拷贝也就是浅拷贝。

但是这里就出现了一个问题,上述我们说到,当变量离开作用域时,会执行drop函数,释放内存,于是s1s2就会都释放一边。因为他们都指向同一片堆区的内容,因此会出现二次释放(double free),这就会造成内存安全问题。

那 Rust 是怎么解决这个问题的呢,试着运行下述代码:

1
2
3
4
let s1 = String::from("hello");
let s2 = s1;

println!("{s1}");

会出现报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:4:15
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{s1}");
| ^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++

For more information about this error, try `rustc --explain E0382`.
warning: `hello_world` (bin "hello_world") generated 1 warning
error: could not compile `hello_world` (bin "hello_world") due to 1 previous error; 1 warning emitted

由报错3 | let s2 = s1; | -- value moved here可以看出,Rust 将s1在拷贝的时候删除了,即直接将s1失效,我们把这个过程称为移动

变量与数据交互的方式(二):克隆

说完浅拷贝(在栈上拷贝),那必不可少的就是深拷贝(还要拷贝堆上的内容)。

Rust 使用函数clone执行深拷贝的操作。

1
2
let s1 = String::from("hello");
let s2 = s1.clone();

不过这里还有一点要注意,浅拷贝拷贝栈上的内容,如果像基本类型的数据,直接赋值是不会触发移动的。

1
2
3
4
let s1 = 5;
let s2 = s1;

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

这里不会出现报错

1
2
3
4
Compiling hello_world v0.1.0 (D:\projects\rust\hello_world)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target\debug\hello_world.exe`
s1=5, s2=5.

Rust 内部有个一个特殊注解Copy trait,实现这样的功能(就变量赋值后能继续用)。

Rust 不允许实现Drop trait 的类型使用Copy trait。也就是说,当一个值离开作用域时,我们不能添加Copy trait

实现Copy trait 的类型:

  • 整形、布尔型、浮点型、字符
  • 元组,当且仅当元组中也都实现了Copy trait

所有权与函数

传入函数的过程与赋值语句差不多,会出现移动和赋值的情况。

我这里直接给出官网的实例

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 s = String::from("hello"); // s 进入作用域

takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效

let x = 5; // x 进入作用域

makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,
// 所以在后面可继续使用 x

} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 没有特殊之处

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{some_string}");
} // 这里,some_string 移出作用域并调用 `drop` 方法。
// 占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{some_integer}");
} // 这里,some_integer 移出作用域。没有特殊之处

注意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
28
29
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 转移给 s1

let s2 = String::from("hello"); // s2 进入作用域

let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 离开作用域并被丢弃

fn gives_ownership() -> String { // gives_ownership 会将
// 返回值移动给
// 调用它的函数

let some_string = String::from("yours"); // some_string 进入作用域。

some_string // 返回 some_string
// 并移出给调用的函数
//
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
//

a_string // 返回 a_string 并移出给调用的函数
}

这时出现一个问题,频繁地转移所有权会很麻烦,我们需要一个操作,不拿到其所有权,但是可以使用它。这就是引用(references)。