author: kwhao
date: 2021-01-21 Fri.

Rust核心功能所有权

所有权(系统)是 Rust 最为与众不同的特性,它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全。

所有权概念

栈(Stack)与堆(Heap)

  1. Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。
  2. 栈:后进先出(last in, first out),增加数据叫做 进栈(pushing onto the stack),移出数据叫做 出栈(popping off the stack)。
  3. 栈中的所有数据都必须占用已知且固定的大小。
  4. 大小未知或大小可能变化的数据,要改为存储在堆上。
  5. 在堆上分配内存(allocating on the heap):内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。
  6. 调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

所有权规则(Ownership Rules)

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

变量作用域(Variable Scope)

作用域是一个项(item)在程序中有效的范围。

  1. ```rust
  2. { // s 在这里无效, 它尚未声明
  3. let s = "hello"; // 从此处起,s 是有效的
  4. // 使用 s
  5. } // 此作用域已结束,s 不再有效
  1. >
  2. > - `s` 进入作用域 时,它就是有效的。
  3. > - 这一直持续到它 离开作用域 为止。
  4. >
  5. <a name="6f512096"></a>
  6. #### `String` 类型(The `String` Type)
  7. 1. String 比基础变量数据类型更复杂
  8. 2. 字符串字面值:程序里手写的那些字符串值。它们是不可变的
  9. 3. Rust提供 `String` 字符串类型,在heap上分配,能够存储在编译时未知数量的文本。
  10. <a name="02fb49d4"></a>
  11. ##### 创建 `String` 类型的值
  12. 1. 使用 `from` 函数基于字符串字面值来创建 `String` :
  13. ```rust
  14. let s = String::from("hello");
  15. // "::"表示 from 是 String 类型下的函数,命名空间
  16. // 可修改需要加上 mut,如: let mut s = ...
  1. 可修改的s
    1. let mut s = String::from("hello");
    2. s.push_str(", world!"); // push_str() 在字符串后追加字面值
    3. println!("{}", s); // 将打印 `hello, world!`

内存与分配(Memory and Allocation)

  1. 字符串字面值在编译时,文本被直接硬编码进最终的可执行文件中。

    速度快、高效。是因为其不可变性。

  2. String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。

    • 必须在运行时向内存分配器(memory allocator)请求内存。
      • 调用 String::from 来实现
  • 需要一个当我们处理完 String 时将内存返回给分配器的方法。
    • 这步在有GC的语言中,GC会跟踪并清理不再使用的内存。
    • 在没有GC的语言中需要自己识别内存何时不再使用,并调用代码返回。
  1. Rust 采用的方法:对于某个值,当拥有他的变量走出作用范围时,内存会立即自动交还给操作系统(调用 drop 函数)。

变量与数据交互的方式(一):移动(Ways Variables and Data Interact: Move)
  1. 多个变量可以与同一个数据使用一种独特的方式来交互
  1. let x = 5;
  2. let y = x;
  3. // 整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。
  1. String 版本
  1. let s1 = String::from("hello");
  2. let s2 = s1;
  3. println!("{}",s1); // s1 已失效,无法编译运行
  1. String的组成:
    • 指针
    • 长度
    • 容量
      上述存放在stack上
    • 字符串内容部分存放在heap上
    • 长度len就是存放字符串内容所需的字节数
    • 容量capacity是 String 从操作系统总共获得的内存字节数
      02所有权 - 图1
  2. s1 赋给 s2String 的数据被复制了一份:
    1. 在stack上复制了一份指针、长度、容量;
    2. 并没有复制指针所指向的heap上的数据;
  3. 变量离开作用域时,Rust自动调用 drop 函数,并将变量使用的heap内存自动释放。
  4. 为了保证内存安全:
    1. Rust没有尝试复制被分配的内存
    2. Rust让 s1 失效:s1 离开作用域时,Rust不需要释放任何东西。
      02所有权 - 图2
  5. 浅拷贝(shallow copy)和 深拷贝(deep copy)
    1. 由于Rust让 s1 失效了,因此不是其他意义上的浅拷贝,Rust称为 移动(Move)
    2. 隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”,故任何 自动 的复制可以被认为对运行时性能影响较小
      02所有权 - 图3

变量与数据交互的方式(二):克隆(Ways Variables and Data Interact: Clone)
  1. let s1 = String::from("hello");
  2. let s2 = s1.clone();
  3. // 针对heap的数据也进行复制
  4. println!("s1 = {}, s2 = {}", s1, s2);

02所有权 - 图4

只在栈上的数据:拷贝(Stack-Only Data: Copy)
  1. let x = 5;
  2. let y = x;
  3. println!("x = {}, y = {}", x, y);
  1. Copy trait:可以用在类似整型这样的存储在stack的类型上。
  2. 如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。
  3. Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。
  4. 一些 Copy 的类型:
    • 所有整数类型,比如 u32
    • 布尔类型,bool,它的值是 truefalse
    • 所有浮点数类型,比如 f64
    • 字符类型,char
    • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所有权与函数(Ownership and Functions)

在语义上,将值传递给函数与给变量赋值相似的:将值传递给函数将发生 移动复制

  1. fn main() {
  2. let s = String::from("hello"); // s 进入作用域
  3. takes_ownership(s); // s 的值移动到函数里 ...
  4. // ... 所以到这里不再有效
  5. let x = 5; // x 进入作用域
  6. makes_copy(x); // x 应该移动函数里,
  7. // 但 i32 是 Copy 的,所以在后面可继续使用 x
  8. } // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  9. // 所以不会有特殊操作
  10. fn takes_ownership(some_string: String) { // some_string 进入作用域
  11. println!("{}", some_string);
  12. } // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
  13. fn makes_copy(some_integer: i32) { // some_integer 进入作用域
  14. println!("{}", some_integer);
  15. } // 这里,some_integer 移出作用域。不会有特殊操作

返回值与作用域(Return Values and Scope)

  1. 返回值也可以转移所有权。
  1. fn main() {
  2. let s1 = gives_ownership(); // gives_ownership 将返回值
  3. // 移给 s1
  4. let s2 = String::from("hello"); // s2 进入作用域
  5. let s3 = takes_and_gives_back(s2); // s2 被移动到
  6. // takes_and_gives_back 中,
  7. // 它也将返回值移给 s3
  8. } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  9. // 所以什么也不会发生。s1 移出作用域并被丢弃
  10. fn gives_ownership() -> String { // gives_ownership 将返回值移动给
  11. // 调用它的函数
  12. let some_string = String::from("hello"); // some_string 进入作用域.
  13. some_string // 返回 some_string 并移出给调用的函数
  14. }
  15. // takes_and_gives_back 将传入字符串并返回该值
  16. fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
  17. a_string // 返回 a_string 并移出给调用的函数
  18. }
  1. 变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
  2. 要函数使用一个值但不获取所有权,可以使用元组来返回多个值,但不推荐,Rust 对此提供了一个功能,叫做 引用(references)
  1. // 使用元组返回的方法,注意不推荐使用这种方法。
  2. fn main() {
  3. let s1 = String::from("hello");
  4. let (s2, len) = calculate_length(s1);
  5. println!("The length of '{}' is {}.", s2, len);
  6. }
  7. fn calculate_length(s: String) -> (String, usize) {
  8. let length = s.len(); // len() 返回字符串的长度
  9. (s, length)
  10. }