这篇教程是现行3个Rust所有权系统之一。所有权系统是Rust最独特且最引人入胜的特性之一,也是作为Rust开发者应该熟悉的。Rust所追求最大的目标 -- 内存安全,关键在于所有权。所有权系统有一些不同的概念,每个概念独自成章:
这3章依次互相关联,你需要完整地阅读全部3章来对Rust的所有权系统进行全面的了解。
原则(Meta)
在我们开始详细讲解之前,这有两点关于所有权系统重要的注意事项。
Rust注重安全和速度。它通过很多零开销抽象(zero-cost abstractions)来实现这些目标,也就是说在Rust中,实现抽象的开销尽可能的小。所有权系统是一个典型的零开销抽象的例子。本文提到所有的分析都是在编译时完成的。你不需要在运行时为这些功能付出任何开销。
然而,这个系统确实有一个开销:学习曲线。很多Rust初学者会经历我们所谓的“与借用检查器作斗争”的过程,也就是指Rust编译器拒绝编译一个作者认为合理的程序。这种“斗争”会因为程序员关于所有权系统如何工作的基本模型与Rust实现的实际规则不匹配而经常发生。当你刚开始尝试Rust的时候,你很可能会有相似的经历。然而有一个好消息:更有经验的Rust开发者反应,一旦他们适应所有权系统一段时间之后,与借用检查器的冲突会越来越少。
记住这些之后,让我们来学习关于所有权的内容。
所有权(Ownership)
Rust中的变量绑定有一个属性:它们有它们所绑定的的值的所有权。这意味着当一个绑定离开作用域,它们绑定的资源就会被释放。例如:
fn foo() {
let v = vec![1, 2, 3];
}
当v
进入作用域,一个新的Vec被创建,向量(vector)也在堆上为它的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)对象和它包含的数据分配了内存。向量对象储存在栈上并包含一个指向堆上[1, 2, 3]
内容的指针。当我们从v
移动到v2
,它为v2
创建了一个那个指针的拷贝。这意味着这将会有两个指向向量内容的指针。这将会因为引入了一个数据竞争而违反Rust的安全保证。因此,Rust禁止我们在移动后使用v
。
注意到优化可能会根据情况移除栈上字节(例如上面的向量)的实际拷贝也是很重要的。所以它也许并不像它开始看起来那样没有效率。
Copy
类型
我们已经建立起了当所有权被转移给另一个绑定时,你不能使用原始绑定的观念后。然而,这里有一个特性会改变这个行为,它叫做Copy
。我们还没有讨论到特性,不过目前,你可以理解为一个为特定类型增加额外行为的标记。例如:
let v = 1;
let v2 = v;
println!("v is: {}", v);
在这个情况,v
是一个i32
,它实现了Copy
特性。这意味着,就像一个移动,当我们把v
赋值给v2
,产生了一个数据的拷贝。不过,不像一个移动,我们仍可以在之后使用v
。这是因为i32
并没有指向其它数据的指针,对它的拷贝是一个完整的拷贝。
我们会在特性部分讨论如何编写你自己类型的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提供了一个特性,借用,它帮助我们解决这个问题。这个主题将在下一个部分讨论!