Rust 的所有权

2022-05-24 20:52:57   最后更新: 2022-05-24 20:52:57   访问数量:163




 

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

 

Rust 环境搭建 Hello World!

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

Rust 基础语法(二) -- 函数与控制流

 

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

 

 

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

 

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

 

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

 

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

 

 

 

 

你可以对可行域的概念并不陌生,在 C、C++、java 等语言中都有可行域的概念,也就是一个变量从声明到生命终结的作用范围:

 

{ // 在声明以前,变量 s 无效 let s = "runoob"; // 这里是变量 s 的可用范围 } // 变量范围已经结束,变量 s 无效

 

 

一旦超出了变量作用的范围,Rust 会自动销毁变量和值,这看起来和很多语言在栈空间中分配内存的行为是一致的。

 

 

4.1 基本类型的移动操作

 

考虑一个简单的赋值操作:

 

fn main() { let x = 5; let y = x; println!("x: {}, y: {}", x, y); }

 

 

这个操作在 C 语言中会为变量 y 开辟一块新的空间,用来将变量 x 的值存储起来,在 Rust 中也是类似的,这样的操作被称为栈中数据的 Move 操作。

 

对于基本数据类型,我们都可以执行这样的 Move 操作:

 

  • 整数类型
  • 布尔类型
  • 浮点类型
  • 字符类型
  • 仅包含以上类型的元组

 

但需要注意的是,只有基本类型可以执行这样的操作,因为他们被分配在栈空间中。而对于字符串、对象、数组等在堆空间中分配的数据来说,这样的操作有着截然不同的行为:

 

fn main() { let x = String::from("hello"); let y = x; println!("x: {}", x); }

 

 

执行上面的代码,会报错:

 

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 都能开辟独立的内存空间,那要怎么办呢?当然也是可以的,这时候只需要执行克隆操作:

 

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

 

 

于是打印出了:

 

s1 = hello, s2 = hello

 

  • 需要注意的是,克隆操作因为需要分配和写入数据,所以要花费更多的时间。

 

 

函数往往需要声明接收外部传入参数,在 Rust 中,此时就必须要关注所有权的转移问题。

 

例如:

 

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 这个字符串数据也就不复存在了。

 

想要让数据不被销毁,只能将数据的所有权以返回值的方式传递到函数外:

 

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.1 引用

 

综上所述,堆空间中分配的数据一旦经过赋值,就会转移所有权,让原变量失效,有时我们并不希望这样,例如在上一节的第一个例子中,虽然我们将 s 作为参数传递给了函数,但因为这个函数的功能仅仅是用来打印 s 的值,我们并不希望数据被销毁,而反复传递所有权又显得过于复杂,有没有更为简单的方法呢?

 

答案是有的,引用可以解决这一问题,对于 C++ 和 java 程序员来说,引用一定不陌生,但在 Rust 语言中却有所不同:

 

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 在函数中传递引用

 

更常用的是,在函数中通过引用来传递参数:

 

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 租借

 

  • 要记住,引用并没有数据的实际所有权,也就是原变量一旦失去数据的所有权,他的所有引用也同时会失效。

 

例如:

 

fn main() { let s1 = String::from("hello"); let s2 = &s1; let s3 = s1; println!("{}", s2); }

 

 

这段代码会报错:

 

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 租借:

 

fn main() { let s1 = String::from("hello"); let mut s2 = &s1; let s3 = s1; s2 = &s3; // 重新从 s3 租借所有权 println!("{}", s2); }

 

 

6.4 可变引用

 

另一个需要注意的点是,上述的引用变量都是不能对数据进行修改的,如果想要让引用的变量能够修改数据,那么就要使用可变引用:

 

fn main() { let mut s1 = String::from("run"); // s1 是可变的 let s2 = &mut s1; // s2 是可变的引用 s2.push_str("oob"); println!("{}", s2); }

 

 

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

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

Rust 专题






rust      所有权     


京ICP备2021035038号