所有权

所有权与堆栈

当你的代码调用一个函数时,传递给函数的参数(包括可能指向堆上数据的指针和函数的局部变量)依次被压入栈中,当函数调用结束时,这些值将被从栈中按照相反的顺序依次移除。 因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 —— 这些数据将永远无法被回收。这就是 Rust 所有权系统为我们提供的强大保障。 对于其他很多编程语言,你确实无需理解堆栈的原理,但是在 Rust 中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助

所有权原则

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

下面通过一些例子深入理解这些原则:

简单介绍String类型

字符串字面值

let s =”hello”s 是被硬编码进程序里的字符串值(类型为 &str )。字符串字面值是很方便的,但是它并不适用于所有场景。原因有二:
  • 字符串字面值是不可变的,因为被硬编码到程序代码中
  • 并非所有字符串的值都能在编写代码时得知
例如,字符串是需要程序运行时,通过用户动态输入然后存储在内存中的,这种情况,字符串字面值就完全无用武之地。 为此,Rust 为我们提供动态字符串类型: String, 该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小未知的文本。

动态字符串类型: String

可以使用下面的方法基于字符串字面量来创建 String 类型:

  1. let s = String::from("hello"); // 创建的值的所有权
:: 是一种调用操作符,这里表示调用 String 中的 from 方法,因为 String 存储在堆上是动态的,你可以这样修改: rust let mut s = String::from("hello"); s.push_str(", world!"); // push_str() 在字符串后追加字面值 println!("{}", s); // 将打印 `hello, world!` ## 所有权的交互 在rust中,复杂数据类型不能自动拷贝,会发生所有权转移。当s1** 被赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了**,rust禁止无效引用,即后面再使用s1会报错。

转移所有权

  • 对于基本类型(存储在栈上),Rust 会自动拷贝,不需要所有权转移。如下:
  1. let x = 5;
  2. let y = x;
  • 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。如String
实际上, String 类型是一个复杂类型,由存储在栈中的堆指针字符串长度字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。 来看一段代码:
  1. let s1 = String::from("hello");
  2. let s2 = s1;
Rust 当中,**s1 被赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2** 后就马上失效了 再来看看,在所有权转移后再来使用旧的所有者,会发生什么:
  1. let s1 = String::from("hello");
  2. let s2 = s1; // 变量绑定
  3. println!("{}, world!", s1);
由于 Rust 禁止你使用无效的引用,你会看到以下的错误:
  1. error[E0382]: borrow of moved value: `s1`
  2. --> src/main.rs:5:28
  3. |
  4. 2 | let s1 = String::from("hello");
  5. | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
  6. 3 | let s2 = s1;
  7. | -- value moved here
  8. 4 |
  9. 5 | println!("{}, world!", s1);
  10. | ^^ value borrowed here after move
  11. |
  12. = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
  13. help: consider cloning the value if the performance cost is acceptable
  14. |
  15. 3 | let s2 = s1.clone();
  16. | ++++++++
  17. For more information about this error, try `rustc --explain E0382`.

解决以上报错,可以使用clone(深拷贝),代码改写如下:

  1. let s1 = String::from("hello");
  2. let s2 = s1.clone();
  3. println!("s1 = {}, s2 = {}", s1, s2);
这段代码能够正常运行,说明 s2 确实完整的复制了 s1 的数据。

克隆(深拷贝)

首先,Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。 如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的方法。 如果代码性能无关紧要,例如初始化程序时或者在某段时间只会执行寥寥数次时,你可以使用 clone 来简化编程。但是对于执行较为频繁的代码(热点路径),使用 clone 会极大的降低程序性能,需要小心使用!

拷贝(浅拷贝)

浅拷贝只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。 再回到之前看过的例子:
  1. let x = 5;
  2. let y = x;
  3. println!("x = {}, y = {}", x, y);
Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。 那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,这里可以给出一个通用的规则: 任何基本类型的组合可以** Copy ,不需要分配内存或某种形式资源的类型是可以 Copy **。如下是一些 Copy 的类型:
  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 true false
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) Copy 的,但 (i32, String) 就不是
  • 不可变引用 &T但是注意: 可变引用 **&mut T** 是不可以 Copy的,参考以下例子:
  1. fn main() {
  2. let x: &str = "hello, world";
  3. let y = x;
  4. println!("{},{}",x,y);
  5. }
这段代码和之前的 String 有一个本质上的区别:在 String 的例子中 s1 持有了通过String::from(“hello”) 创建的值的所有权,而这个例子中,x 只是引用了存储在二进制中的字符串 “hello, world”,并没有持有所有权。因此 let y = x 中,仅仅是对该引用进行了拷贝,此时 yx 都引用了同一个字符串。

引用与借用

获取变量的引用,称之为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。

引用与解引用

所有权和借用 - 图1

不可变引用

正如变量默认不可变一样,引用指向的值默认也是不可变的
  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. }

报错如下:

  1. error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
  2. --> src/main.rs:8:5
  3. |
  4. 7 | fn change(some_string: &String) {
  5. | ------- help: consider changing this to be a mutable reference: `&mut String`
  6. ------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String`
  7. 8 | some_string.push_str(", world");
  8. | ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  9. `some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改

可变引用

只需要一个小调整,即可修复上面代码的错误:
  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. }
首先,声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。

可变引用同时只能存在一个

不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制: 同一作用域,特定数据只能有一个可变引用
  1. let mut s = String::from("hello");
  2. let r1 = &mut s;
  3. let r2 = &mut s;
  4. println!("{}, {}", r1, r2);
以上代码会报错:
  1. error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间无法对 `s` 进行两次可变借用
  2. --> src/main.rs:5:14
  3. |
  4. 4 | let r1 = &mut s;
  5. | ------ first mutable borrow occurs here 首个可变引用在这里借用
  6. 5 | let r2 = &mut s;
  7. | ^^^^^^ second mutable borrow occurs here 第二个可变引用在这里借用
  8. 6 |
  9. 7 | println!("{}, {}", r1, r2);
  10. | -- first borrow later used here 第一个借用在这里使用
对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器 borrow checker 特性之一,不过各行各业都一样,限制往往是出于安全的考虑,Rust 也一样。 这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:
  • 两个或更多的指针同时访问同一数据
  • 至少有一个指针被用来写入数据
  • 没有同步数据访问的机制
数据竞争会导致未定义行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码! 解决方法:大括号可以帮我们解决一些编译不通过的问题,通过手动限制变量的作用域: rust let mut s = String::from("hello"); { let r1 = &mut s; } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用 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);
错误如下:
  1. error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  2. // 无法借用可变 `s` 因为它已经被借用了不可变
  3. --> src/main.rs:6:14
  4. |
  5. 4 | let r1 = &s; // 没问题
  6. | -- immutable borrow occurs here 不可变借用发生在这里
  7. 5 | let r2 = &s; // 没问题
  8. 6 | let r3 = &mut s; // 大问题
  9. | ^^^^^^ mutable borrow occurs here 可变借用发生在这里
  10. 7 |
  11. 8 | println!("{}, {}, and {}", r1, r2, r3);
  12. | -- immutable borrow later used here 不可变借用在这里使用
Rust 1.31 新版编译器解决以上报错方法:
  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. }
  10. // 新编译器中,r3作用域在这里结束

悬垂引用

在 Rust 中编译器可以确保引用永远也不会变成悬垂状态(指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用)。 让我们尝试创建一个悬垂引用,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. error[E0106]: missing lifetime specifier
  2. --> src/main.rs:5:16
  3. |
  4. 5 | fn dangle() -> &String {
  5. | ^ expected named lifetime parameter
  6. |
  7. = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
  8. help: consider using the `'static` lifetime
  9. |
  10. 5 | fn dangle() -> &'static String {
  11. | ~~~~~~~~

错误信息:this function’s return type contains a borrowed value, but there is no value for it to be borrowed from. 该函数返回了一个借用的值,但是已经找不到它所借用值的来源。

本质原因:因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,所以不对!

解决方法是直接返回 String
  1. fn no_dangle() -> String {
  2. let s = String::from("hello");
  3. s
  4. }
这样就没有任何错误了,最终 String所有权被转移给外面的调用者

借用规则总结

  • 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用(无法同时借用可变和不可变

可变引用不能借给多个人,不然可能会因为多个人因为修改变量而产生矛盾;而不可变引用就没有这个问题,因为他们没有权限修改,所以不会发生矛盾。

  • 引用必须总是有效的

参考文章

引用与借用 - Rust语言圣经(Rust Course)