程序管理运行时计算机内存的方式:

  • 具有垃圾回收机制
  • 程序员自己分配与释放内存
  • Rust 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

    背景:栈与堆 入栈比在堆上分配内存要快,因为入栈时分配器无需为存储新数据去搜索内存空间;其位置总是栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。

所有权系统要处理的问题:

  • 跟踪哪部分代码正在使用堆上的哪些数据
  • 最大限度的减少堆上的重复数据数量
  • 清理堆上不再使用的数据确保不会耗尽空间

    所有权规则

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

    变量作用域

    1. { // s 在这里无效,它尚未声明
    2. let s = "hello"; // 从此处起,s 是有效的
    3. // 使用 s
    4. } // 此作用域已结束,s 不再有效

    这里有两个重要的时间点:

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

    内存与分配

    以 Rust String 类型为例,为了支持一个可变、可增长的文本片段,需要在 上分配一块在编译时未知大小的内存来存放内容。这意味着:

  • 必须在运行时向内存分配器(memory allocator)请求内存。

  • 需要一个当我们处理完 String 时将内存返回给分配器的方法。

第一部分由 String::from完成,它的实现请求其所需的内存。
第二部分在 Rust 是内存在拥有它的变量离开作用域后就被自动释放,当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop。

  1. {
  2. let s = String::from("hello"); // 从此处起,s 是有效的
  3. // 使用 s
  4. } // 此作用域已结束,
  5. // s 不再有效

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

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

image.png
当 s2 和 s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
为了确保内存安全,在 let s2 = s1 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。

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

这段代码会无法编译,因为 s1 不再生效。

  1. $ cargo run
  2. Compiling ownership v0.1.0 (file:///projects/ownership)
  3. error[E0382]: borrow of moved value: `s1`
  4. --> src/main.rs:5:28
  5. |
  6. 2 | let s1 = String::from("hello");
  7. | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
  8. 3 | let s2 = s1;
  9. | -- value moved here
  10. 4 |
  11. 5 | println!("{}, world!", s1);
  12. | ^^ value borrowed here after move
  13. For more information about this error, try `rustc --explain E0382`.
  14. error: could not compile `ownership` due to previous error

如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是浅拷贝。
image.png
上图是 s1 无效之后的内存表现。

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

如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。

  1. let s1 = String::from("hello");
  2. let s2 = s1.clone();
  3. println!("s1 = {}, s2 = {}", s1, s2);

image.png

只在栈上的数据:拷贝

  1. let x = 5;
  2. let y = x;
  3. println!("x = {}, y = {}", x, y);

原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
Rust 有一个叫做 Copy trait 的特殊注解,如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。支持 Copy 的类型:

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

    所有权与函数

    ```rust fn main() { let s = String::from(“hello”); // s 进入作用域

    takes_ownership(s); // s 的值移动到函数里 …

    1. // ... 所以到这里不再有效

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

    makes_copy(x); // x 应该移动函数里,

    1. // 但 i32 是 Copy 的,
    2. // 所以在后面可继续使用 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 移出作用域。没有特殊之处

  1. <a name="i7rOU"></a>
  2. ## 返回值与作用域
  3. ```rust
  4. fn main() {
  5. let s1 = gives_ownership(); // gives_ownership 将返回值
  6. // 转移给 s1
  7. let s2 = String::from("hello"); // s2 进入作用域
  8. let s3 = takes_and_gives_back(s2); // s2 被移动到
  9. // takes_and_gives_back 中,
  10. // 它也将返回值移给 s3
  11. } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  12. // 所以什么也不会发生。s1 离开作用域并被丢弃
  13. fn gives_ownership() -> String { // gives_ownership 会将
  14. // 返回值移动给
  15. // 调用它的函数
  16. let some_string = String::from("yours"); // some_string 进入作用域.
  17. some_string // 返回 some_string
  18. // 并移出给调用的函数
  19. //
  20. }
  21. // takes_and_gives_back 将传入字符串并返回该值
  22. fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
  23. //
  24. a_string // 返回 a_string 并移出给调用的函数
  25. }

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有
如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进去再返回来就有点烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。

  1. fn main() {
  2. let s1 = String::from("hello");
  3. let (s2, len) = calculate_length(s1);
  4. println!("The length of '{}' is {}.", s2, len);
  5. }
  6. fn calculate_length(s: String) -> (String, usize) {
  7. let length = s.len(); // len() 返回字符串的长度
  8. (s, length)
  9. }

Rust 对此提供了一个不用获取所有权就可以使用值的功能,叫做 引用(references)。