1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。
所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。

栈(Stack)与堆(Heap)

http://120.78.128.153/rustbook/ch04-01-what-is-ownership.html

String

字符串字面值,即被硬编码进程序里的字符串值
  1. { // s 在这里无效, 它尚未声明
  2. let s = "hello"; // 从此处起,s 是有效的
  3. // 使用 s
  4. }
  1. let mut s = String::from("hello");
  2. s.push_str(", world!"); // push_str() 在字符串后追加字面值
  3. println!("{}", s); // 将打印 `hello, world!`
为什么 String 可变而字面值却不行呢? 保存在不同内存位置 栈 堆

内存与分配

对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
  • 必须在运行时向操作系统请求内存。
  • 需要一个当我们处理完 String 时将内存返回给操作系统的方法。
第一部分由我们完成:当调用 String::from 时,它的实现 (implementation) 请求其所需的内存。这在编程语言中是非常通用的。 然而,第二部分实现起来就各有区别了。在有 垃圾回收garbage collectorGC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确的为一个 allocate 配对一个 free Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
  1. {
  2. let s = String::from("hello"); // 从此处起,s 是有效的
  3. // 使用 s
  4. } // 此作用域已结束,
  5. // s 不再有效
当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop

移动

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

所有权 - 图1 所有权 - 图2

当我们将 s1 赋值给 s2String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。 并不是像下图这样,如果 Rust 这么做了,那么操作 s2 = s1 在堆上数据比较大的时候会对运行时性能造成非常大的影响。

所有权 - 图3

由于两个数据指针指向了同一位置。这就有了一个问题:当 s2s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 为了确保内存安全,与其尝试拷贝被分配的内存,Rust 则认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。
  1. let s1 = String::from("hello");
  2. let s2 = s1;
  3. println!("{}, world!", s1);
  4. // 报错

浅拷贝shallow copy)和 深拷贝deep copy

拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动move),而不是浅拷贝。 上面的例子可以解读为 s1移动 到了 s2 中。

所有权 - 图4

这样就解决了我们的问题!因为只有 s2 是有效的,当其离开作用域,它就释放自己的内存,完毕。 另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。

克隆

如果我们 确实 需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。
  1. let s1 = String::from("hello");
  2. let s2 = s1.clone();
  3. println!("s1 = {}, s2 = {}", s1, s2);

拷贝

触发:只在栈上的数据

像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。 Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。 具有Copy trait的类型
  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 true false
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是。

所有权与函数

将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
  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 移出作用域。不会有特殊操作
当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。

返回值和作用域

  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. }
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 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)。

引用和借用

  1. fn main() {
  2. let s1 = String::from("hello");
  3. let len = calculate_length(&s1);
  4. println!("The length of '{}' is {}.", s1, len);
  5. }
  6. fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
  7. s.len()
  8. } // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  9. // 所以什么也不会发生
& 符号就是 引用,它允许你使用值但不获取其所有权。 与使用 & 引用相反的操作是 解引用dereferencing),它使用解引用运算符 *

所有权 - 图5

变量 s 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。 我们将获取引用作为函数参数称为 借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
  1. fn main() {
  2. let s = String::from("hello");
  3. change(&s);
  4. }
  5. fn change(some_string: &String) {
  6. some_string.push_str(", world");
  7. }
  8. // 报错
正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。

可变引用

  1. fn main() {
  2. let mut s = String::from("hello");
  3. change(&mut s);
  4. }
  5. fn change(some_string: &mut String) {
  6. some_string.push_str(", world");
  7. }
不过可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用。这些代码会失败:
  1. let mut s = String::from("hello");
  2. let r1 = &mut s;
  3. let r2 = &mut s;
  4. println!("{}, {}", r1, r2);
这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争data race)类似于竞态条件,它可由这三个行为造成:
  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。
  1. // 可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时 拥有:
  2. let mut s = String::from("hello");
  3. {
  4. let r1 = &mut s;
  5. } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
  6. let r2 = &mut s;
  1. let mut s = String::from("hello");
  2. let r1 = &s; // 没问题
  3. let r2 = &s; // 没问题
  4. let r3 = &mut s; // 大问题
  5. println!("{}, {}, and {}", r1, r2, r3);
  6. // 报错
我们 不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。 注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。
  1. let mut s = String::from("hello");
  2. let r1 = &s; // 没问题
  3. let r2 = &s; // 没问题
  4. println!("{} and {}", r1, r2);
  5. // 此位置之后 r1 和 r2 不再使用
  6. let r3 = &mut s; // 没问题
  7. println!("{}", r3);
不可变引用 r1r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。它们的作用域没有重叠,所以代码是可以编译的。

悬垂引用(Dangling References)

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。
  1. fn main() {
  2. let reference_to_nothing = dangle();
  3. }
  4. fn dangle() -> &String {
  5. let s = String::from("hello");
  6. &s
  7. }
  8. // 报错
  1. fn dangle() -> &String { // dangle 返回一个字符串的引用
  2. let s = String::from("hello"); // s 是一个新字符串
  3. &s // 返回字符串 s 的引用
  4. } // 这里 s 离开作用域并被丢弃。其内存被释放。
  5. // 危险!
  1. // solution
  2. fn no_dangle() -> String {
  3. let s = String::from("hello");
  4. s // 所有权转移出去
  5. }

引用的规则

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

Slice

  1. fn first_word(s: &String) -> usize {
  2. let bytes = s.as_bytes();
  3. for (i, &item) in bytes.iter().enumerate() {
  4. if item == b' ' {
  5. return i;
  6. }
  7. }
  8. s.len()
  9. }
因为需要逐个元素的检查 String 中的值是否为空格,需要用 as_bytes 方法将 String 转化为字节数组。 接下来,使用 iter 方法在字节数组上创建一个迭代器。 iter 方法返回集合中的每一个元素,而 enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回。enumerate 返回的元组中,第一个元素是索引,第二个元素是集合中元素的引用。
  1. fn main() {
  2. let mut s = String::from("hello world");
  3. let word = first_word(&s); // word 的值为 5
  4. s.clear(); // 这清空了字符串,使其等于 ""
  5. // word 在此处的值仍然是 5,
  6. // 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效!
  7. }
这个程序编译时没有任何错误,而且在调用 s.clear() 之后使用 word 也不会出错。因为 words 状态完全没有联系,所以 word 仍然包含值 5。可以尝试用值 5 来提取变量 s 的第一个单词,不过这是有 bug 的,因为在我们将 5 保存到 word 之后 s 的内容已经改变。

字符串 slice

字符串 slicestring slice)是 String 中一部分值的引用

  1. let s = String::from("hello world");
  2. let hello = &s[0..5];
  3. let world = &s[6..11];
可以使用一个由中括号中的 [starting_index..ending_index] 指定的 range 创建一个 slice,其中 starting_index 是 slice 的第一个位置,ending_index 则是 slice 最后一个位置的后一个值。 在其内部,slice 的数据结构存储了 slice 的开始位置和长度,长度对应于 ending_index 减去 starting_index 的值。 let world = &s[6..11];

所有权 - 图6

对于 Rust 的 .. range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。
  1. let s = String::from("hello");
  2. let slice = &s[0..2];
  3. let slice = &s[..2];
如果 slice 包含 String 的最后一个字节,也可以舍弃尾部的数字。
  1. let s = String::from("hello");
  2. let len = s.len();
  3. let slice = &s[3..len];
  4. let slice = &s[3..];
也可以同时舍弃这两个值来获取整个字符串的 slice
  1. let s = String::from("hello");
  2. let len = s.len();
  3. let slice = &s[0..len];
  4. let slice = &s[..];

// 字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出。

  1. fn first_word(s: &String) -> &str {
  2. let bytes = s.as_bytes();
  3. for (i, &item) in bytes.iter().enumerate() {
  4. if item == b' ' {
  5. return &s[0..i];
  6. }
  7. }
  8. &s[..]
  9. }
  10. fn main() {
  11. let mut s = String::from("hello world");
  12. let word = first_word(&s);
  13. s.clear(); // 错误!
  14. println!("the first word is: {}", word);
  15. }
  16. // 16 产生不可变引用 18 产生可变引用

字符串字面值就是 slice

  1. let s = "Hello, world!";
这里 s 的类型是 &str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。

字符串 slice 作为参数

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

其他类型的 slice

  1. let a = [1, 2, 3, 4, 5];
  2. let slice = &a[1..3];
这个 slice 的类型是 &[i32]。它跟字符串 slice 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。