知己知彼

  • Rust是静态类型的语言
  • Rust不提供Garbage Collector
  • Rust会进行大量的编译时优化

所有权-Ownership

一定要理解Rust的所有权细节, 事实上, 在这一部分我发现Rust更像是C语言家族中的一份子了。 这一部分基于“The Rust Programming Language”相应章节翻译而来。

所有权ownership是Rust最具独到的特点,它使得Rust在不依靠垃圾收集器的情况下也能保证内存安全。因此,理解所有权在Rust中的工作至关重要。

所有权伴还随着其他几个概念:

  • 转移(Move)
  • 借用(Borrow)
  • 切片(Slice)

什么是所有权

所有权是Rust的核心概念。在面对内存管理这一任务时,程序总会出现分配-回收这一基本行为,有些语言实现了GC,它使用单独的线程不断地去探测那些内存已经不被使用,丞需回收;有些语言则允许程序员手动分配和回收内存。Rust选择了第三条路:内存管理经由一些列规则约束的所有权机制完成,这一行为发生的时机是在编译时。所有权机制不会减缓程序运行。

相比于Java日渐庞大的虚拟机,Rust在一开始就放弃了GC,这使得它不必面对早期虚拟机GC影响性能的难题。

随着开发者越来越熟悉Rust及其所有权机制,写出安全高效的代码只是家常便饭。不过对于很多程序员来说,所有权的确是个新玩意儿,需要花一定的时间去熟悉它。

接下来我们用字符串作为案例学习。

所有权规则

  • 对于Rust中的每个值,都存在名为其“所有者”的变量。
  • 同一时间只能存在一个“所有者”。
  • 当“所以者”退出其所在的变量空间时,其对应的值会被抛弃。

变量空间

变量空间显然是对于变量而言的,当变量在某个范围内有效的时候,这个范围就是它的变量空间。
假设在main函数中声明一个字符串:

  1. fn main{ // s 无效,因为还未定义
  2. let s = "RUST"; // s 从此时有效
  3. } // 变量空间结束,s无效

其他许多语言中也有类似概念。

String 类型

后面会讲到 String&str 的区分。

Rust中是可以写直接字符串的,也就是

  1. let s = "Hello Rust!";

String 是复合类型,其名称空间下的 from 函数也可以生成字符串:

  1. let mut ss = String::from("Hello Rust!");
  2. ss.push_str("!!!!"); // 这是字面量没有的功能
  3. println!("{}", s);

虽然你可以写出 let mut s = "SHFOA"; 这样令字面量成为mut的代码,但编译器所认为的字面量仍然是不可变的,硬编码的字面量直接编译到可执行文件中,又快又准。因为编译器知道你需要多少空间,使用多长时间。而如果字面量也可变,那么就无法再编译时确定字符串的大小,也就无法将其放入内存中。 因此,我们还需要一个更高级的 String

String::from 会尝试获取需要分配的空间大小,这就意味着在将来的某一时间,还需要对这同样的一块空间回收。我们已经知道Rust没有GC,所以我们需要让编译器知道,什么时候String不再对我们有用,可以被抛弃。

一个十分符合直觉的套路是:如果我知道何时分配(allocate),我只需要在使用完String后显示地(deallocate/drop)就可以了。这正是C++所做的,在对象的生命周期结束时唤起回收。(Resource Acquisition Is Initialization)。在Rust中,drop的调用是自动进行的,它按照所有权的规则行事。

变量和数据的交互:转移(Move)

Rust的内存区域大体来说也是分栈(Stack)和堆(Heap)的, 当你在栈上分配内存时(通常是标量scalar),
每次赋值都会创建新的值:

  1. let x = 5; // Push 5 into stack
  2. let y = x; // Push another 5 into stack, now there are two 5

但是换做String就不一样了:

  1. let s1 = String::from("Hello");
  2. let s2 = s1;
  3. // println!("{}", s1); // Invalid s1, beacause it has been moved to s2.

trpl04-02.svg
啊哈,典型的浅拷贝(Shallow Copy)。两个变量都指向同一块堆内存,但正如上文所言,Rust在变量移动出其所在的空间时自动进行drop行为,由于drop是变量绑定的,drop完s1还要drop s2吗?显然这并不合理,事实上这会引发double free error,造成内存污染。

Rust的处理看起来有些“专横”:在你浅拷贝s1至s2时,编译器会认为s1的任务已经结束(因为s2也指向同一段内存只是名称不同),进而直接drop s1。所以在 let s2 = s1; 时发生的事情其实是这样:
move_rust.png
在Rust中,这一行为叫做转移(Move)。你可以将其理解为使源变量失效的浅拷贝。

至于深拷贝,Rust则没有玩太多花样,只要源变量实现了 clone 方法即可。对于 String 来说,就是:

  1. let s2 = s1.clone();
  2. println!("Both s1{} and s2{} are valid!", s1, s2);

对一般的标量,比如Integer,是直接“深拷贝”(实际上Integer的深浅拷贝无区别)。 但对于深拷贝本身来说,Rust把它实现为了一个特质(Trait),你可以理解为Java中的Cloneable接口。 不是所有的变量都适合/实现了这个特质。通常来说,标量都是有Copy特质的,复合中的元组Tuple,如果其中的成员类型都是标量,则它也是有Copy特质的,否则不然。

所有权和函数

函数,它代表的新的变量空间。传参会导致转移或深拷贝(以下简称“复制”)

  • 如果变量是转移进去的(比如一个String),那么该变量在此函数出栈时随之消亡。
  • 如果变量是复制进去的(比如一个char),那么该变量在此函数出栈时,在调用此函数的变量空间中还有效。 ```rust n main() { let s = String::from(“hello”); // s comes into scope

    takes_ownership(s); // s’s value moves into the function…

    1. // ... and so is no longer valid here

    let x = 5; // x comes into scope

    makes_copy(x); // x would move into the function,

    1. // but i32 is Copy, so it’s okay to still
    2. // use x afterward

} // Here, x goes out of scope, then s. But because s’s value was moved, nothing // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope println!(“{}”, some_string); } // Here, some_string goes out of scope and drop is called. The backing // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope println!(“{}”, some_integer); } // Here, some_integer goes out of scope. Nothing special happens.

  1. 但是,函数是可以有返回值的,对于返回值的所有权,直接回被移交给调用方:
  2. ```rust
  3. fn main() {
  4. let s1 = gives_ownership(); // gives_ownership moves its return
  5. // value into s1
  6. let s2 = String::from("hello"); // s2 comes into scope
  7. let s3 = takes_and_gives_back(s2); // s2 is moved into
  8. // takes_and_gives_back, which also
  9. // moves its return value into s3
  10. //-----------------------------------------------------------------------
  11. let s2 = String::from("hello"); // s2 comes into scope again
  12. let (s2, len) = calculate_length(s2);
  13. println!("{}-{{{},{}}}-{}", s1, s2, len, s3);
  14. // ----------------------------------------------------------------------
  15. } // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  16. // moved, so nothing happens. s1 goes out of scope and is dropped.
  17. fn gives_ownership() -> String { // gives_ownership will move its
  18. // return value into the function
  19. // that calls it
  20. let some_string = String::from("hello"); // some_string comes into scope
  21. some_string // some_string is returned and
  22. // moves out to the calling
  23. // function
  24. }
  25. // takes_and_gives_back will take a String and return one
  26. fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
  27. // scope
  28. a_string // a_string is returned and moves out to the calling function
  29. }
  30. fn calculate_length(s: String) -> (String, usize) {
  31. let length = s.len(); // len() returns the length of a String
  32. (s, length)
  33. }

注意,由于s2被转移,虽然所有权通过函数返回值重新被s3获得,而且值也相同,但s2已经不可用了。 这里体现了转移是基于浅拷贝的性质。

如果我不想讲s2转移呢?因为s2的所有权相当于被函数消耗掉了,那么我们只需要再把它原模原样返回给s2就可以了,通过返回元组,我们依然可以使用s2。(这实际上是配合了遮盖shadowing)

显然,在其他高级语言中不会有这么多破事,而我们又确实需要很多不消耗所有权的函数,那该怎么办呢?
不能转移,就靠借用吧,好借好还嘛。

引用和借用(Borrow)

引用:References,正如其它语言中的引用类型一样。但是,在Rust中,引用不是单独出现的,通常是人为地进行“引用(Referencing)”和“解引用(Dereferencing)”。

所以我们可以暂且理解为,“转移”是值传递,“借用”是引用传递。
Rust使用 & 作为引用标记符号。(相对的,解引用使用 * 符号)

在下面的例子中,函数进行引用传递,所有权并没有发生转移,也不存在s1失效。

  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 {
  7. s.len()
  8. }

refer_rust.png

关键点

1.
但还没完,借用的变量默认是不可变的。(也即:引用默认是imutable)

这很容易理解,好比借出去的书给你只读就可以了,但你想要再写写画画就过分了。(安全性)

2.
话虽如此,使用mut可以使一个引用变量成为可变的,前提是,这个变量本身是 mut 修饰的,且在同一变量空间仅存在唯一一个可变引用。(有点类似写互斥锁)

  1. #[allow(unused)]
  2. fn main() {
  3. let mut s = String::from("hello");
  4. change(&mut s);
  5. let r1 = &mut s;
  6. // let r2 = &mut s; // 问题代码,已存r1这个可变引用,且r1在r2定义时仍为valid。
  7. // println!("{}, {}", r1, r2);
  8. }
  9. fn change(some_string: &mut String) {
  10. some_string.push_str(", world");
  11. }

同样以借书为例,就算允许进行修改,同一时间也只能由一人操作。(阻止竞争) Rust在编译时就会小心资源竞争。典型的资源竞争场景比如:

  • 两个或多个指针同时访问同一段内存。
  • 至少有一个指针已经用于写数据。
  • 不存在同步(synchronize)访问机制的读写

3.
所以,不存在多个可变引用满足的是不可同时修改条件,也就是说,如果程序可以放弃同时性, 你可以通过创建新的变量空间容许其他可变引用存在一段时间:

  1. let mut s = String::from("hello");
  2. {
  3. let r1 = &mut s;
  4. } // r1 goes out of scope here, so we can make a new reference with no problems.
  5. let r2 = &mut s;

创建变量空间只需要一对花括号…

4.
基于上述推断,凡是可变引用出现的地方,还要保持一致性,因此,在Rust中你也不能在不可变引用使用结束前定义或使用可变引用:

  1. let mut s = String::from("hello");
  2. let r1 = &s; // no problem
  3. let r2 = &s; // no problem
  4. let r3 = &mut s; // BIG PROBLEM!!!!!!!!
  5. println!("{}, {}, and {}", r1, r2, r3);

到这里你可以看出这基本就是对某一资源的读写互斥行为。

在上面的例子中,如果r1,r2在r3定义前的变量空间中有使用,且在r3定义后无使用,则可以编译通过(Rust编译器还是比较灵活的):

  1. let mut s = String::from("hello");
  2. let r1 = &s; // no problem
  3. let r2 = &s; // no problem
  4. println!("{} and {}", r1, r2);
  5. // r1 and r2 are no longer used after this point
  6. let r3 = &mut s; // no problem
  7. println!("{}", r3);

悬挂指针/引用

Dangling Pointers,指针玩家最不希望看到这种事情发生。

Rust编译时会避免引用悬挂,也就是说,只要编译通得过,通常你不用考虑引用安不安全。

假设你想要返回一个局部变量的引用:

  1. fn main() {
  2. let reference_to_nothing = dangle();
  3. }
  4. fn dangle() -> &String {
  5. let s = String::from("hello");
  6. &s
  7. }

编译的错误信息中会有这么一句:

  1. this function's return type contains a borrowed value, but there is no value
  2. for it to be borrowed from.

这是在说,引用的传递(也即“借用”行为)需要被借用者自身是可用的/存在的。而函数的变量空间显然结束了局部变量 s 的性命,返回它的引用不符合常理。实际上,如果你想要使用函数中的局部变量,直接使用转移就可以了:

  1. fn no_dangle() -> String {
  2. let s = String::from("hello");
  3. s
  4. }

切片(Slice)

功能上类似于Python的切片,但Rust的Slice更独特。

Slice是一种数据类型,它和引用一样不获取所有权。它就像是专门为连续数据设计的游标(cursor),同时还可以让你在不遍历整个集合的情况下获得某一特定的部分内容。

切片比索引(index)更好的地方在于,它直接绑定到对象上,而索引则是使用数字下标这一弱联系。

字符串切片

  1. let s = String::from("hello world");
  2. let hello = &s[0..5];
  3. let world = &s[6..11];
  4. let slice = &s[..2]; // let slice = &s[0..2];
  5. let slice = &s[3..]; // let slice = &s[3..s.len()];
  6. let slice = &s[..]; // let slice = &s[0..s.len()];

这种 num1..num2 的形式,在迭代里面也出现过。 区间左闭右开。

Rust默认UTF-8编码, 也就是最长4字节的字符,这也是slice可以接收的范围,对于多字节字符符号, slice需要特殊处理。

Slice由于绑定到了对象/资源身上,所以在建立引用时需要满足一些关键点(见上文),否则Slice会立即出现编译时错误。

注意:字面量字符串是&str,也就是字符串切片。&str是不可变引用。

String&str

  • 前者是类型,后者是切片。
  • 前者可以通过 from 转字面量为 String 类型,后者可以通过 to_string 转为 String 类型。
  • 使用 &String 是String类型的引用,使用 &str 是切片本身。

比如,如果一个函数接收字符串作为参数(假定这里是借用),究竟是传递String类型引用,还是字符串切片呢?
官方教程认为使用切片更加灵活,因为切片即可以表示整体字符串(&String代表的)也可以表示一部分字符串(&str有能力这样做)。

  1. fn main() {
  2. let my_string = String::from("hello world");
  3. // first_word works on slices of `String`s
  4. let word = first_word(&my_string[..]);
  5. let my_string_literal = "hello world";
  6. // first_word works on slices of string literals
  7. let word = first_word(&my_string_literal[..]);
  8. // Because string literals *are* string slices already,
  9. // this works too, without the slice syntax!
  10. let word = first_word(my_string_literal);
  11. }

由于String是一种类型(Type),它可以被Struct持有而不必考虑其他生命周期。 &str作为引用放在Struct里时需要额外的生命周期参数。