Rust 的所有权机制

2022-06-27 17:07:47 浏览数 (1)

1. 引言

前面的文章中,我们介绍了 Rust 的基本语法:

Rust 环境搭建 Hello World!

Rust 基础语法(一) -- 变量、运算与注释

Rust 基础语法(二) -- 函数与循环

可以看到,Rust 的语法与很多其他语言的基础语法非常类似,那么 Rust 真正的独特之处在哪里呢?就在于它的内存管理方式,本文就来详细介绍一下。

2. Rust 所有权

C/C 语言需要我们手动去管理内存,而 java、python 等语言则拥有他们自己的内存垃圾回收机制,Rust 区别于上述这些语言的一大特点就是能够高效地使用内存,而这背后的机制就是“所有权”机制。

Rust 所有权的规则有以下三条:

  1. 每个值都有一个变量,称为这个值的所有者;
  2. 一个值一次只能有一个所有者;
  3. 当所有者不在程序运行范围时,该值将被删除。

初看这三条规则可能不太好理解,别急,我们接着来介绍。

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);
 }

但需要注意的是,一个变量可以有多个不可变引用,但只能有一个可变引用。一旦一个值被可变引用,它就不能再被任何引用。

0 人点赞