这篇指南是 Rust 已经存在的三个所有权制度之一。这是 Rust 最独特和最令人信服的一个特点,其中 Rust 开发人员应该相当熟悉。所有权即 Rust 如何实现其最大目标和内存安全。这里有几个不同的概念,每一个概念都有它自己的章节:
-
所有权,即正在读的这篇文章。
-
借用,和与它们相关的功能‘引用’
- 生存期,借用的先进理念
这三篇文章相关且有序。如果你想完全的理解所有权制度,你将需要这三篇文章。
元
在我们了解细节之前,这里有关于所有权制度的两个重要说明需要知道。
Rust 注重安全和速度。它通过许多‘零成本抽象’来完成这些目标,这意味着在 Rust 中,用尽可能少的抽象成本来保证它们正常工作。所有权制度是一个零成本抽象概念的一个主要例子。我们将在这篇指南中提到的所有分析都是在编译时完成的。你不用为了任何功能花费任何运行成本。
然而,这一制度确实需要一定的成本:学习曲线。许多新用户使用我们喜欢称之为‘与借检查器的人斗争’,即 Rust 编译器拒绝编译那些作者认为有效的程序的 Rust 经验。这往往因为程序员对于所有权的怎样工作与 Rust 实现的规则不相符的心理模型而经常出现。你可能在第一次时会经历类似的事情。然而有个好消息:更有经验的 Rust 开发者报告称,一旦他们遵从所有权制度的规则工作一段时间后,他们会越来越少的与借检查器的行为斗争。
学习了这些后,让我们来了解所有权。
所有权
变量绑定在 Rust 中有一个属性:它们有它们绑定到的变量的‘所有权’。这意味着当一个绑定超出范围时,将释放它们绑定到资源。例如:
fn foo() {
let v = vec![1, 2, 3];
}
当 v 进入范围时,将新创建新的 Vec<T>。在这种情况下,向量也在堆上为三个元素分配空间。当 v 超出 foo() 函数的作用域时,Rust 将清除一切与向量有关的东西,也包括为堆分配的内存。在该作用域结束时,这种情况就一定会发生。
移动语义
尽管这里也有很多微妙的东西:Rust 确保任何给定的资源都有一个确定的绑定。例如,如果我们有一个向量,我们可以将它赋值给另一个绑定。
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]);
同样的错误:‘移动值使用’。当我们将所有权转移给其他东西时,我们可以说我们已经‘移动’了我们提到的东西。这里你不需要某种特殊注释,它是 Rust 默认做的事情。
详细信息
当我们移动一个绑定后,我们不可以使用这个绑定的原因是微妙的,但是很重要。当我们编写如下代码时:
let v = vec![1, 2, 3];
let v2 = v;
第一行为向量对象 v 和它包含的内容分配内存。向量对象存放在栈中,同时包含一个指向存放在堆中的内容 ( [1, 2, 3] ) 的指针。当我们将 v 赋值给 v2 时,它将为 v2 创建一个这个指针的副本。这意味着将会有两个指针指向堆中的向量内容。它将引进数据竞争,这违反了 Rust 的安全保障。因此,Rust 禁止在我们移动后使用 v。
我们需同样注意某种情况下优化可能删除在栈中字节的真正副本。所以它并不像最初看起来的那样毫无效率。
复制类型
在我们将所有权转移到另一个绑定时,我们已经建立了,你不可以使用原来的绑定。然而,这里有一个特性可以改变这种行为,它被称为 Copy 。我们还没有讨论过这个特性,但是现在,你可以把它们看作是增加额外行为的一个特殊类型的一个注释。例如:
let v = 1;
let v2 = v;
println!("v is: {}", v);
在这种情况下,v 是一个 i32,这实现了 Copy 的特性。这意味着,就像一个移动,当我们将 v 赋值给 v2 时,我们就完成了数据的一个副本。但是,与移动不同,我们在之后仍然可以使用 v。这是因为 i32 在别处没有指向数据的指针,这是一个完整的副本。
我们将在特性章节讨论怎样完成你自己类型的复制。
不仅仅是所有权
当然,如果我们不得不将每个函数的所有权交回,我们可以写如下代码: