参考:https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.htmlhttps://doc.rust-lang.org/book/ch04-01-what-is-ownership.html

栈、堆、所有权

管理计算机内存的方式:

  1. 垃圾回收机制:在程序运行时不断地寻找不再使用的内存
  2. 程序员必须亲自分配和释放内存
  3. 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查

栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。

栈 (Stack)

  1. 以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出(last in, first out)
  2. 增加数据叫做 进栈(pushing onto the stack);移出数据叫做 出栈(popping off the stack)
  3. 栈中的所有数据都必须占用已知且固定的大小,在编译时将数据推入栈中并不被认为是分配
  4. 指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针
  5. 当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
  6. 类比叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!

    堆 (Heap)

  7. 大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。

  8. 操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。
  9. 入栈比在堆上分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
  10. 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。
  11. 处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
  12. 类比:假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。

    所有权系统 (ownership) 与栈、堆

  13. 所有权要处理的事情:跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间

  1. 一旦理解了所有权,就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。

所有权

所有权的规则:

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

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

String

String 类型可以使用 from 函数基于字符串字面值来创建String,比如let s = String::from("hello");,s 是 String 类型,而 "hello" 是 “str 类型”,这个语句意味着运行时向操作系统请求内存请求 String 所需的内存,而不是把数据硬编码进二进制文件。
可以修改此类字符串 :s.push_str(", world!"); // push_str() 在字符串后追加字面值
支持一个可变,可增长的文本片段,所以需要在堆上分配一块在编译时未知大小的内存来存放内容:这意味着

  • 必须在运行时向操作系统请求内存。
  • 需要一个当我们处理完 String 时将内存返回给操作系统的方法 ```

    ![allow(unused)]

    fn main() { { let s = String::from(“hello”); // 从此处起,s 是有效的 // 可使用 s } // 此作用域已结束,
    1. // s 不再有效
    }
  1. String 类型的变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 `drop`,在这里 `String` 的作者可以放置释放内存的代码。Rust 在结尾的 `}` 处自动调用 `drop`。<br />析构函数 destructor:与构造函数相反,指当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行的函数,在 Rust 中即实现 Drop trait,但是 drop 一般针对堆中数据,具有 Copy trait 的数据是不会被 drop 的,因为 "Copy trait 不允许存在 Drop trait"
  2. > C++ 中,这种 item 在生命周期结束时释放资源的模式有时被称作 **资源获取即初始化** _Resource Acquisition Is Initialization (RAII)_ )。
  3. 不定长度的数据类型不支持 Copy trait,比如下面的例子在 Rust `s1`被**移动**(move) 到了`s2`中,这充分反映了所有权规则:对象的指针只被一个变量所拥有,不存在“内存共享”。

let s1 = String::from(“hello”); let s2 = s1; println!(“{}, world!”, s1); // error: value used here after move

  1. ![image.png](assets/image-20210331201100-1w1r5is.png)<br />如果我们 **确实** 需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的通用函数。

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

  1. 此外有一个设计选择:Rust 永远也不会**自动**创建数据的 “深拷贝”(手动创建可定义 "Clone trait")。因此,任何**自动**的复制("Copy trait")可以被认为对运行时性能影响较小。<br />这是自动复制的例子:

let x = 5; let y = x; // i32 是 Copy 的,所以在后面可继续使用 x println!(“{} {}”, x, y);

  1. `5` 绑定到 `x`;接着生成一个值 `x` 的拷贝并绑定到 `y`”。现在有了两个变量,`x` `y`,都等于 `5`。因为整数是有已知固定大小的简单值,所以这两个 `5` 被放入了栈中。

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

  1. takes_ownership(s); // s 的值移动到函数里 ...
  2. // ... 所以到这里不再有效
  3. let x = 5; // x 进入作用域
  4. makes_copy(x); // x 应该移动函数里,
  5. // 但 i32 是 Copy 的,所以在后面可继续使用 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. ## Clone / Copy trait
  2. > 在一些语言中有深浅拷贝的概念:
  3. > **浅拷贝** 只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。也就是简单的内存拷贝(或者C语言的按位拷贝 memcpy)。
  4. > **深拷贝** 会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
  5. 但是 Rust 更倾向于使用 Copy trait Clone trait 来描述 浅拷贝 深拷贝:<br />Copy trait 可以看成浅拷贝,但是有一些规则和特殊性:
  6. - Copy 的全名是 `std::marker::Copy`。而 `std::marker` 模块里面的所有的 trait 都是特殊的 trait。目前稳定的有四个: CopySendSizedSync。它们的特殊之处在于它们是跟编译器密切绑定的,impl 这些 trait 对编译器的行为有重要影响。在编译器眼里,它们与其它的 trait 不一样。这几个 trait 内部都没有方法,它们的唯一任务是,给类型打一个“标记”,表明它符合某种约定,这些约定会影响编译器的静态检查以及代码生成。
  7. - 如果一个类型 impl Copy trait,意味着任何时候,只拷贝存储在栈上的位来复制值而不需要额外的代码,来实现该类型的复制。这意味着 变量绑定、函数参数传递、函数返回值传递等场景,使用 copy 语义,而不是默认的 move 语义
  8. - 只有 "POD 数据类型" 才有资格实现 Copy trait,但不是所有的 POD 类型都应该实现 Copy trait
  9. - 对于 struct enum 类型,不会自动实现 Copy trait。而且只有当 struct enum 内部每个元素都是Copy类型的时候,编译器才允许我们针对此类型实现(derive) Copy trait
  10. - Copy 操作通常在栈上拷贝,而且编译器可以优化实现 Copy trait 的类型,这意味着无需显式调用 Clone/Copy trait,这让代码更简洁
  11. - 任何使用 Copy trait 的代码都可以通过 Clone trait 实现,但代码可能会稍慢,或者不得不在代码中的许多位置上使用 Clone trait
  12. 类比 C 风格 [POD](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Passive_data_structure) (Plain Old Data,或者叫 PDS: Passive Data Structure) 概念,Rust 也可以有 POD 的数据类型,只要一种 struc 或类型同时满足:
  13. 1. 只包含特定数据类型的成员:整数、浮点数、布尔 bool、字符类型 char、所有元素都是 Copy trait 数组或元组、共享借用指针&
  14. 1. 没有指针类型的成员
  15. 1. 没有自定义"析构函数",即自身或其任何部分没有 Drop trait
  16. 不能实现 Copy trait 的类型:Box String Vec、(i32, String) 之类存在不可 Copy trait 元素的元组、[String, String] 之类的容纳不可 Copy trait 元素的数组、可写借用指针&mut<br />例子:"整数自动 Copy"
  17. ---
  18. Clone trait 不仅仅是深拷贝,它的特殊性:
  19. 1.
  20. Clone 的全名是 `std::clone::Clone` 。它的完整声明是这样的:

pub trait Clone : Sized { fn clone(&self) -> Self; fn clone_from(&mut self, source: &Self) { *self = source.clone() } }

  1. 它有两个关联方法,其中 clone_from 是有默认实现的,它依赖于 clone 方法的实现。clone 方法没有默认实现,需要我们手动实现,甚至可以根据情况在 clone 函数中编写**任意的逻辑**。
  2. 1.
  3. Clone trait 一般用于“基于语义的复制”操作。它做什么事情,跟具体类型的作用息息相关。比如对于 Box 类型,clone 就是执行的“深拷贝”,而对于 Rc 类型,clone 做的事情就是把引用计数值加1
  4. 1.
  5. 对于实现了 Copy trait 的类型,它的 Clone trait 应该跟 Copy 语义相容,等同于按位拷贝。
  6. 参考:[知乎:Clone VS Copy](https://zhuanlan.zhihu.com/p/21730929)、[Rust Book:变量与数据交互的方式(二):克隆](https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html#%E5%8F%98%E9%87%8F%E4%B8%8E%E6%95%B0%E6%8D%AE%E4%BA%A4%E4%BA%92%E7%9A%84%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%85%8B%E9%9A%86)、[Rust Book:复制值的 Clone 和 Copy](https://kaisery.github.io/trpl-zh-cn/appendix-03-derivable-traits.html#%E5%A4%8D%E5%88%B6%E5%80%BC%E7%9A%84-clone-%E5%92%8C-copy)
  7. # 引用 (Reference)
  8. ## 不变引用 / 不变借用 (Borrow)
  9. 不变引用 (reference):
  10. - 允许使用值但不获取其所有权,当引用离开作用域时其指向的值不会被丢弃,但是引用(的指针地址)和引用绑定的变量名会被丢弃
  11. - 使用方法: `&变量名` 相应的类型 `&类型`
  12. - 作用域:从声明的地方开始一直持续到最后一次使用为止
  13. - 可以和多个不变引用同时使用
  14. 不变借用 (borrowing):将获取引用作为函数参数。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。

fn main() { let s = String::from(“hello”); let s1 = &s; // &x: reference 引用 let s2 = f(&s); // &x: borrowing 借用 println!(“{} {} {}”, s, s1, s2); // 结果:hello hello hello }

fn f(s: &String) -> &String { s }

  1. ![image.png](assets/image-20210401195010-9f89vmt.png)
  2. ## 可变引用 / 可变借用
  3. - 对引用/借用的变量进行修改
  4. - 使用方法: `&mut 变量名` 相应的类型 `&mut 类型`
  5. - 在特定作用域中的特定数据只能有一个可变引用

fn main() { let mut s = String::from(“hello”); change(&mut s); println!(“{}”, s); // hello, world }

fn change(some_string: &mut String) { some_string.push_str(“, world”); }

  1. 引用/借用的规则:
  2. 1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。不能在拥有不可变引用的同时拥有可变引用。<br />
  3. _(注意理解:在任意给定时间、拥有 这些词语的含义)_:不变引用不能和可变引用同时使用<br />
  4. 这并不是说不变引用和可变引用在代码里面只能出现一种,而是两种引用都存在的情况下,必须其中一种引用结束才能使用另一种引用。<br />
  5. 比如 所有的可变引用必须离开作用域之后才能使用不变引用,这样如果可变引用改变数据就完全不会那些不变引用。
  6. 1. 引用必须总是有效的。<br />
  7. Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。<br />
  8. 悬垂指针(dangling pointer):释放内存时保留指向它的指针,这是一种错误且无效行为,因为悬垂指针指向的内存可能已经被分配给其它持有者。
  9. 来看一段同时使用两种引用的错误的代码:

fn main() { let mut s = String::from(“hello”); let s1 = &mut s; // 可变引用在此处被声明 // 我们没有利用可变引用改写值 println!(“{} {}”, s, s1); // 出错:cannot borrow s as immutable because it is also borrowed as mutable }

  1. ```
  2. error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
  3. --> src/main.rs:4:23
  4. |
  5. 3 | let s1 = &mut s;
  6. | ------ mutable borrow occurs here
  7. 4 | println!("{} {}", s, s1);
  8. | ^ -- mutable borrow later used here
  9. | |
  10. | immutable borrow occurs here

因为如果可变引用中途改写了数据,而不变引用的值意外被改变了: println!("{} {}", s, s1.push('a')); 这样的语句会造成 “数据竞争”,因此编译器禁止可变引用与不可变引用同时发挥作用。
注意:println! 形式上捕获了变量 s,而实际上捕获了 s 的不变引用 &s。所以这一行语句同时拥有不变引用和可变引用,而这恰好违反了引用的规则。
再看一段符合引用规则的例子:同时使用多个不变引用(因为它们不影响数据的值),然后使用一个可变引用(无论实际上有没有改变值)。

  1. fn main() {
  2. let mut s = String::from("hello");
  3. let r1 = &s; // 没问题
  4. let r2 = &s; // 没问题
  5. println!("{} and {}", r1, r2);
  6. // 此位置之后 r1 和 r2 不再使用
  7. let r3 = &mut s; // 没问题
  8. println!("{}", r3);
  9. }

数据竞争(data race)类似于竞态条件,它可由这三个行为造成:

  1. 两个或更多指针同时访问同一数据。(引用并不是直接让指针访问数据,引用 “首先返回的是指针”)
  2. 至少有一个指针被用来写入数据。
  3. 没有同步数据访问的机制。

数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
参考:https://kaisery.github.io/trpl-zh-cn/ch04-02-references-and-borrowing.html

slice 类型

和引用一样,是一种没有所有权的数据类型。slice (切片) 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
slice 和数组类似,但其大小在编译时是不确定的。
slice 是一个双字 对象(two-word object),第一个字是一个指向数据的指针,第二个字是切片的长度。这 个 “字” 的宽度和 usize 相同,由处理器架构决定,比如在 x86-64 平台上就是 64 位。 slice 可以用来借用数组的一部分。slice 的类型标记为&[T]
一些 slice 类型的例子:比如通过返回部分字符串的索引来同步访问部分字符串的值、返回部分数组的索引来同步访问部分数组的值

  • “字符串字面值” str 就是 slice:比如 let s = "Hello, world!"; ,其中 s 的类型是 &str,即一个指向二进制程序特定位置的 slice,一个不可变引用。所以字符串字面值是不可变的。
  • 其他具体类型的 slice 具有 &变量名[x..y] 的形式(x 到 y 是左闭右开,x 和 y 可以省略一个或者都省略):”String 类型” 的 slice 具体类型是 &str,”数组” 的 slice 具体类型是 &[数组元素类型]
    此外,range 还可以用 = 来表明需要取到右端点: &变量名[x..=y]
    数组 slice 例子:
  1. fn main() {
  2. let a = [1, 2, 3, 4, 5];
  3. let slice = &a[1..3]; // slice 类型: &[i32]
  4. println!("{:?}", slice); // [2, 3]
  5. }

字符串相关的 slice 例子:获取第一个单词

  1. #![allow(unused)]
  2. fn first_word(s: &str) -> &str {
  3. let bytes = s.as_bytes();
  4. for (i, &item) in bytes.iter().enumerate() {
  5. if item == b' ' {
  6. return &s[0..i];
  7. }
  8. }
  9. &s[..]
  10. }
  11. fn main() {
  12. let my_string = String::from("hello world");
  13. // first_word 中传入 `String` 的 slice
  14. let word = first_word(&my_string[..]); // word 类型:&str
  15. let my_string_literal = "hello world"; // my_string_literal 类型:&str
  16. // first_word 中传入字符串字面值的 slice
  17. let word = first_word(&my_string_literal[..]);
  18. // 因为字符串字面值 **就是** 字符串 slice,
  19. // 这样写也可以,即不使用 slice 语法!
  20. let word = first_word(my_string_literal);
  21. }

参考:https://kaisery.github.io/trpl-zh-cn/ch04-03-slices.html