十四、 Rust 所有权 Ownership

编程语言把内存分为两大类:

  • 栈 stack
  • 堆 heap

当然了,这两种分类并没有对实际的内存做什么,只是把系统分给应用程序的内存标识为上面的两大类而已。

14.1 栈 stack

栈 stack 是一种 后进先出 容器。就像我们的存储罐子,后面放进去的只能先拿出来(后面放进去的会放在上面)。

栈 stack 上存储的元素大小必须是已知的,也就是说如果一个变量或数据要放到栈上,那么它的大小在编译是就必须是明确的。

例如,对于一个数据类型为 i32 的变量,它的大小是可预知的,只占用 4 个字节。

Rust 语言中所有的标量类型都可以存储到栈上,因为它们的大小都是固定的。

而对于字符串这种复合类型,它们在运行时才会赋值,那么在编译时的大小就是未知的。那么它们就不能存储在栈上,而只能存储在 上。

14.2 堆 heap

堆 heap 用于存储那些在编译时大小未知的数据,也就是那些只有在运行时才能确定大小的数据。

我们一般在堆 heap 上存储那些动态类型的数据。简而言之,我们一般在堆上存储那些可能在程序的整个生命周期中发生变化的数据。

是不受系统管理的,由用户自己管理,因此,使用不当,内存溢出的可能性就大大增加了。

14.3 什么是所有权 ?

所有权就是一个东西属不属于你,你有没有权力随意处理它,比如送人,比如扔掉。

Rust 语言中每一值都有一个对应的变量,这个变量就成为这个值的 所有者。从某些方面说,定义一个变量就是为这个变量和它存储的数据定义一种所有者管理,声明这个值由这个变量所有。

例如,对于 let age = 30 这条语句,相当于声明 30 这个值由变量 age 所有。

这个比喻是不恰当的。变量并不是对 30 这个数字拥有,而是某个内存块的所有者,而这个内存块上存储者 30 这个数。

任何东西只有一个所有者,Rust 中是不允许有共同所有者这个概念的。

Rust 中,任何特定时刻,一个数据只能有一个所有者。

Rust 中,不允许两个变量同时指向同一块内存区域。变量必须指向不同的内存区域。

14.3.1 转让所有权

既然所有权就是一个东西属不属于你,你有没有权力随意处理它,比如送人,比如扔掉。

那么转让所有权就会时不时的发生。

Rust 语言中转让所有权的方式有以下几种:

把一个变量赋值给另一个变量。重要

把变量传递给函数作为参数。

函数中返回一个变量作为返回值。

接下来我们分别对这三种方式做详细的介绍

14.3.2 把一个变量赋值给另一个变量

Rust 自己宣称的最大卖点是它的 内存安全,这也是它认为能够取代 C++ 作为系统级别语言的自信之一。

Rust 为了实现内存安全,Rust 严格控制谁可以使用内存和什么时候应该限制使用内存。

说起来有点晦涩难懂,我们直接看代码,然后通过代码来解释

  1. fn main(){
  2. // 向量 v 拥有堆上数据的所有权
  3. // 每次只能有一个变量对堆上的数据拥有所有权
  4. let v = vec![1,2,3];
  5. // 赋值会导致两个变量都对同一个数据拥有所有权
  6. // 因为两个变量指向了相同的内存块
  7. let v2 = v;
  8. // Rust 会检查两个变量是否同时拥有堆上内存块的所有权。
  9. // 如果发生所有权竞争,它会自动将所有权判给给新的变量
  10. // 运行出错,因为 v 不再拥有数据的所有权
  11. println!("{:?}",v);
  12. }

上面的代码中我们首先声明了一个向量 v。所有权的概念是只有一个变量绑定到资源,v 绑定到资源或 v2 绑定到资源。

上面的代码会发生编译错误 use of moved value: v。这是因为赋值操作会将资源的所有权转移到了 v2。这意味着所有权从 v 移至 v2 ( v2 = v ),移动后 v 就会变得无效。

14.3.3 把变量传递给函数作为参数

将堆中的对象传递给闭包或函数时,值的所有权也会发生变更

  1. fn main(){
  2. let v = vec![1,2,3]; // 向量 v 拥有堆上数据的所有权
  3. let v2 = v; // 向量 v 将所有权转让给 v2
  4. display(v2); // v2 将所有权转让给函数参数 v ,v2 将变得不可用
  5. println!("In main {:?}",v2); // v2 变得不可用
  6. }
  7. fn display(v:Vec<i32>){
  8. println!("inside display {:?}",v);
  9. }

编译运行以上 Rust 代码,报错如下

  1. error[E0382]: borrow of moved value: `v2`
  2. --> src/main.rs:5:28
  3. |
  4. 3 | let v2 = v; // 向量 v 将所有权转让给 v2
  5. | -- move occurs because `v2` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
  6. 4 | display(v2);
  7. | -- value moved here
  8. 5 | println!("In main {:?}",v2);
  9. | ^^ value borrowed here after move

修复的关键,就是注释掉最后的输出 v2

  1. fn main(){
  2. let v = vec![1,2,3]; // 向量 v 拥有堆上数据的所有权
  3. let v2 = v; // 向量 v 将所有权转让给 v2
  4. display(v2); // v2 将所有权转让给函数参数 v ,v2 将变得不可用
  5. //println!("In main {:?}",v2); // v2 变得不可用
  6. }
  7. fn display(v:Vec<i32>){
  8. println!("inside display {:?}",v);
  9. }

编译运行以上 Rust 代码,报错如下

  1. inside display [1, 2, 3]

14.3.4 函数中返回一个变量作为返回值

传递给函数的所有权将在函数执行完成时失效。

也就是函数的形参获得的所有权将在离开函数后就失效了。失效了数据就再也访问不到的了。

为了解决所有权失效的问题,我们可以让函数将拥有的对象返回给调用者。

  1. fn main(){
  2. let v = vec![1,2,3]; // 向量 v 拥有堆上数据的所有权
  3. let v2 = v; // 向量 v 将所有权转让给 v2
  4. let v2_return = display(v2);
  5. println!("In main {:?}",v2_return);
  6. }
  7. fn display(v:Vec<i32>)-> Vec<i32> {
  8. // 返回同一个向量
  9. println!("inside display {:?}",v);
  10. return v;
  11. }

编译运行上面的 Rust 代码,输出结果如下

  1. inside display [1, 2, 3]
  2. In main [1, 2, 3]

14.4 所有权和基本(原始)数据类型

所有的基本数据类型,把一个变量赋值给另一个变量,并不是所有权转让,而是把数据复制给另一个对象。简单的说,就是在内存上重新开辟一个区域,存储复制来的数据,然后把新的变量指向它。

这样做的原因,是因为原始数据类型并不需要占用那么大的内存。

  1. fn main(){
  2. let u1 = 10;
  3. let u2 = u1; // u1 只是将数据拷贝给 u2
  4. println!("u1 = {}",u1);
  5. }

编译运行以上 Rust 代码,输出结果如下

  1. u1 = 10

注意:所有权只会发生在堆上分配的数据,对比 C++,可以说所有权只会发生在指针上。基本类型的存储都在栈上,因此没有所有权的概念。