1. 引言
前面的文章中,我们介绍了 Rust 的基本语法:
Rust 环境搭建 Hello World!
Rust 基础语法(一) -- 变量、运算与注释
Rust 基础语法(二) -- 函数与循环
可以看到,Rust 的语法与很多其他语言的基础语法非常类似,那么 Rust 真正的独特之处在哪里呢?就在于它的内存管理方式,本文就来详细介绍一下。
2. Rust 所有权
C/C 语言需要我们手动去管理内存,而 java、python 等语言则拥有他们自己的内存垃圾回收机制,Rust 区别于上述这些语言的一大特点就是能够高效地使用内存,而这背后的机制就是“所有权”机制。
Rust 所有权的规则有以下三条:
- 每个值都有一个变量,称为这个值的所有者;
- 一个值一次只能有一个所有者;
- 当所有者不在程序运行范围时,该值将被删除。
初看这三条规则可能不太好理解,别急,我们接着来介绍。
3. 变量的范围
你可以对可行域的概念并不陌生,在 C、C 、java 等语言中都有可行域的概念,也就是一个变量从声明到生命终结的作用范围:
代码语言:javascript复制 {
// 在声明以前,变量 s 无效
let s = "runoob";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效
一旦超出了变量作用的范围,Rust 会自动销毁变量和值,这看起来和很多语言在栈空间中分配内存的行为是一致的。
4. 移动和克隆
4.1 基本类型的移动操作
考虑一个简单的赋值操作:
代码语言:javascript复制 fn main() {
let x = 5;
let y = x;
println!("x: {}, y: {}", x, y);
}
这个操作在 C 语言中会为变量 y 开辟一块新的空间,用来将变量 x 的值存储起来,在 Rust 中也是类似的,这样的操作被称为栈中数据的 Move 操作。
对于基本数据类型,我们都可以执行这样的 Move 操作:
- 整数类型
- 布尔类型
- 浮点类型
- 字符类型
- 仅包含以上类型的元组
但需要注意的是,只有基本类型可以执行这样的操作,因为他们被分配在栈空间中。而对于字符串、对象、数组等在堆空间中分配的数据来说,这样的操作有着截然不同的行为:
代码语言:javascript复制 fn main() {
let x = String::from("hello");
let y = x;
println!("x: {}", x);
}
执行上面的代码,会报错:
代码语言:javascript复制 warning: unused variable: `y`
--> ownership.rs:3:9
|
3 | let y = x;
| ^ help: if this is intentional, prefix it with an underscore: `_y`
|
= note: `#[warn(unused_variables)]` on by default
error[E0382]: borrow of moved value: `x`
--> ownership.rs:4:23
|
2 | let x = String::from("hello");
| - move occurs because `x` has type `String`, which does not implement the `Copy` trait
3 | let y = x;
| - value moved here
4 | println!("x: {}", x);
| ^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0382`.
对于堆空间中分配的数据的所有者来说,在执行 let y = x 语句后,x 变量已经无效了,这个数据的所有权被移交给了 y,此后,x 是不能被使用的。
4.2 克隆
那么,即便是对于堆内存中的数据,我也仍然想要让 x、y 都能开辟独立的内存空间,那要怎么办呢?当然也是可以的,这时候只需要执行克隆操作:
代码语言:javascript复制 fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
于是打印出了:
s1 = hello, s2 = hello
- 需要注意的是,克隆操作因为需要分配和写入数据,所以要花费更多的时间。
5. 变量在函数中的所有权机制
函数往往需要声明接收外部传入参数,在 Rust 中,此时就必须要关注所有权的转移问题。
例如:
代码语言:javascript复制 fn main() {
let s = String::from("hello");
takes_ownership(s);
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
在 main 函数中,由于将 s 所有的字符串数据的所有权转移给了函数的传入参数 some_string,在调用函数后,变量 s 便不能再进行使用,而在函数中,随着函数的结束,some_string 也会随着作用域结束而被释放。于是,hello 这个字符串数据也就不复存在了。
想要让数据不被销毁,只能将数据的所有权以返回值的方式传递到函数外:
代码语言:javascript复制 fn main() {
let s = String::from("hello");
let x = takes_ownership(s);
println!("x: {}", x);
}
fn takes_ownership(some_string: String) -> String {
println!("{}", some_string);
return some_string;
}
6. 引用与租借
6.1 引用
综上所述,堆空间中分配的数据一旦经过赋值,就会转移所有权,让原变量失效,有时我们并不希望这样,例如在上一节的第一个例子中,虽然我们将 s 作为参数传递给了函数,但因为这个函数的功能仅仅是用来打印 s 的值,我们并不希望数据被销毁,而反复传递所有权又显得过于复杂,有没有更为简单的方法呢?
答案是有的,引用可以解决这一问题,对于 C 和 java 程序员来说,引用一定不陌生,但在 Rust 语言中却有所不同:
代码语言:javascript复制 fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}
可以看到,通过 & 操作符,让 s2 成为了 s1 的引用,s1 并不会失效,这是因为 s2 仅仅租借了 s1 对数据的所有权,只要 s1 持有这个数据的所有权,s2 也就可以对数据进行操作,但 s2 并没有数据的实际所有权。
6.2 在函数中传递引用
更常用的是,在函数中通过引用来传递参数:
代码语言:javascript复制 fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
6.3 租借
- 要记住,引用并没有数据的实际所有权,也就是原变量一旦失去数据的所有权,他的所有引用也同时会失效。
例如:
代码语言:javascript复制 fn main() {
let s1 = String::from("hello");
let s2 = &s1;
let s3 = s1;
println!("{}", s2);
}
这段代码会报错:
代码语言:javascript复制 warning: unused variable: `s3`
--> reference.rs:4:9
|
4 | let s3 = s1;
| ^^ help: if this is intentional, prefix it with an underscore: `_s3`
|
= note: `#[warn(unused_variables)]` on by default
error[E0505]: cannot move out of `s1` because it is borrowed
--> reference.rs:4:14
|
3 | let s2 = &s1;
| --- borrow of `s1` occurs here
4 | let s3 = s1;
| ^^ move out of `s1` occurs here
5 | println!("{}", s2);
| -- borrow later used here
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0505`.
因为 s2 租借的 s1 已经将所有权移动到 s3,所以 s2 将无法继续租借使用 s1 的所有权。如果需要使用 s2 使用该值,必须重新向 s3 租借:
代码语言:javascript复制 fn main() {
let s1 = String::from("hello");
let mut s2 = &s1;
let s3 = s1;
s2 = &s3; // 重新从 s3 租借所有权
println!("{}", s2);
}
6.4 可变引用
另一个需要注意的点是,上述的引用变量都是不能对数据进行修改的,如果想要让引用的变量能够修改数据,那么就要使用可变引用:
代码语言:javascript复制 fn main() {
let mut s1 = String::from("run");
// s1 是可变的
let s2 = &mut s1;
// s2 是可变的引用
s2.push_str("oob");
println!("{}", s2);
}
但需要注意的是,一个变量可以有多个不可变引用,但只能有一个可变引用。一旦一个值被可变引用,它就不能再被任何引用。