5.8.所有权
最后更新于:2022-04-01 00:43:43
这篇教程是现行3个Rust所有权系统之一。所有权系统是Rust最独特且最引人入胜的特性之一,也是作为Rust开发者应该熟悉的。Rust所追求最大的目标 -- 内存安全,关键在于所有权。所有权系统有一些不同的概念,每个概念独自成章:
* 所有权,你正在阅读的这个章节
* [借用](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.9.References%20and%20Borrowing%20%E5%BC%95%E7%94%A8%E5%92%8C%E5%80%9F%E7%94%A8.md),以及它关联的特性: "引用" (references)
* [生命周期](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.10.Lifetimes%20%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.md),关于借用的高级概念
这3章依次互相关联,你需要完整地阅读全部3章来对Rust的所有权系统进行全面的了解。
## 原则(Meta)
在我们开始详细讲解之前,这有两点关于所有权系统重要的注意事项。
Rust注重安全和速度。它通过很多_零开销抽象_(_zero-cost abstractions_)来实现这些目标,也就是说在Rust中,实现抽象的开销尽可能的小。所有权系统是一个典型的零开销抽象的例子。本文提到所有的分析都是**在编译时完成的**。你不需要在运行时为这些功能付出任何开销。
然而,这个系统确实有一个开销:学习曲线。很多Rust初学者会经历我们所谓的“与借用检查器作斗争”的过程,也就是指Rust编译器拒绝编译一个作者认为合理的程序。这种“斗争”会因为程序员关于所有权系统如何工作的基本模型与Rust实现的实际规则不匹配而经常发生。当你刚开始尝试Rust的时候,你很可能会有相似的经历。然而有一个好消息:更有经验的Rust开发者反应,一旦他们适应所有权系统一段时间之后,与借用检查器的冲突会越来越少。
记住这些之后,让我们来学习关于所有权的内容。
## 所有权(Ownership)
Rust中的[变量绑定](http://doc.rust-lang.org/stable/book/variable-bindings.html)有一个属性:它们有它们所绑定的的值的_所有权_。这意味着当一个绑定离开作用域,它们绑定的资源就会被释放。例如:
~~~
fn foo() {
let v = vec![1, 2, 3];
}
~~~
当`v`进入作用域,一个新的[Vec](http://doc.rust-lang.org/stable/std/vec/struct.Vec.html)被创建,向量(vector)也在[堆](http://doc.rust-lang.org/stable/book/the-stack-and-the-heap.html)上为它的3个元素分配了空间。当`v`在`foo()`的末尾离开作用域,Rust将会清理掉与向量(vector)相关的一切,甚至是堆上分配的内存。这在作用域的结尾是一定(deterministically)会发生的。
## 移动语义
然而这里有更巧妙的地方:Rust确保了对于任何给定的资源都_正好(只)有一个_绑定与之对应。例如,如果我们有一个向量(vector),我们可以把它赋予另外一个绑定:
~~~
let v = vec![1, 2, 3];
let v2 = v;
~~~
不过,如果之后我们尝试使用`v`,我们得到一个错误:
~~~
let v = vec![1, 2, 3];
let v2 = v;
println!("v[0] is: {}", v[0]);
~~~
它看起来像这样:
~~~
error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
^
~~~
当我们定义了一个取得所有权的函数,并尝试在我们把变量作为参数传递给函数之后使用这个变量时,会发生相似的事情:
~~~
fn take(v: Vec<i32>) {
// what happens here isn’t important.
}
let v = vec![1, 2, 3];
take(v);
println!("v[0] is: {}", v[0]);
~~~
一样的错误:“use of moved value”。当我们把所有权转移给别的别的绑定时,我们说我们“移动”了我们引用的值。这里你并不需要什么类型的特殊注解,这是Rust的默认行为。
## 细节
在移动了绑定后我们不能使用它的原因是微妙的,也是重要的。当我们写了这样的代码:
~~~
let v = vec![1, 2, 3];
let v2 = v;
~~~
第一行为向量(vector)对象和它包含的数据分配了内存。向量对象储存在[栈](http://doc.rust-lang.org/stable/book/the-stack-and-the-heap.html)上并包含一个指向[堆](http://doc.rust-lang.org/stable/book/the-stack-and-the-heap.html)上`[1, 2, 3]`内容的指针。当我们从`v`移动到`v2`,它为`v2`创建了一个那个指针的拷贝。这意味着这将会有两个指向向量内容的指针。这将会因为引入了一个数据竞争而违反Rust的安全保证。因此,Rust禁止我们在移动后使用`v`。
注意到优化可能会根据情况移除栈上字节(例如上面的向量)的实际拷贝也是很重要的。所以它也许并不像它开始看起来那样没有效率。
## `Copy`类型
我们已经建立起了当所有权被转移给另一个绑定时,你不能使用原始绑定的观念后。然而,这里有一个[特性](http://doc.rust-lang.org/stable/book/traits.html)会改变这个行为,它叫做`Copy`。我们还没有讨论到特性,不过目前,你可以理解为一个为特定类型增加额外行为的标记。例如:
~~~
let v = 1;
let v2 = v;
println!("v is: {}", v);
~~~
在这个情况,`v`是一个`i32`,它实现了`Copy`特性。这意味着,就像一个移动,当我们把`v`赋值给`v2`,产生了一个数据的拷贝。不过,不像一个移动,我们仍可以在之后使用`v`。这是因为`i32`并没有指向其它数据的指针,对它的拷贝是一个完整的拷贝。
我们会在[特性](http://doc.rust-lang.org/stable/book/traits.html)部分讨论如何编写你自己类型的`Copy`。
## 所有权之外(More than ownership)
当然,如果我们不得不在每个我们写的函数中交还所有权:
~~~
fn foo(v: Vec<i32>) -> Vec<i32> {
// do stuff with v
// hand back ownership
v
}
~~~
这将会变得烦人。它在我们获取更多变量的所有权时变得更糟:
~~~
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with v1 and v2
// hand back ownership, and the result of our function
(v1, v2, 42)
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let (v1, v2, answer) = foo(v1, v2);
~~~
额!返回值,返回的代码行(上面的最后一行),和函数调用都变得更复杂了。
幸运的是,Rust提供了一个特性,借用,它帮助我们解决这个问题。这个主题将在下一个部分讨论!