- 第1节:Rust 介绍
- 第2节:Rust 安装
- 第3节:Rust Hello,world!
- 第4节:Rust Hello,Cargo!
- 第5节:Rust 栈和堆
- 第6节:Rust 测试
- 第7节:Rust 条件编译
- 第8节:Rust 文档
- 第9节:Rust 迭代器
- 第10节:Rust 并发性
- 第11节:Rust 错误处理
- 第12节:Rust 外部函数接口
- 第13节:Rust Borrow 和 AsRef
- 第14节:Rust 发布通道
- 第15节:Rust 变量绑定
- 第16节:Rust 函数
- 第17节:Rust 基本类型
- 第18节:Rust 注释
- 第19节:Rust if
- 第20节:Rust for 循环
- 第21节:Rust while 循环
- 第22节:Rust 所有权
- 第23节:Rust 引用与借用
- 第24节:Rust 生存期
- 第25节:Rust 可变性
- 第26节:Rust 结构体
- 第27节:Rust 枚举
- 第28节:Rust 匹配
- 第29节:Rust 模式
- 第30节:Rust 方法语法
- 第31节:Rust 向量
- 第32节:Rust 字符串
- 第33节:Rust 泛型
- 第34节:Rust 特征
- 第35节:Rust 降
- 第36节:Rust if let
- 第37节:Rust 特征的对象
- 第38节:Rust 闭包
- 第39节:Rust 通用函数调用语法
- 第40节:Rust 箱和模块
- 第41节:Rust “常量”和“静态”
- 第42节:Rust 属性
- 第43节:Rust type 别名
- 第44节:Rust 类型转换
- 第45节:Rust 关联类型
- 第46节:Rust 全类型
- 第47节:Rust 操作符和重载
- 第48节:Rust 'Deref'强制转换
- 第49节:Rust 宏命令
- 第50节:Rust 原始指针
- 第51节:Rust unsafe
Rust 引用与借用
引用与借用
这篇指南是 Rust 已经存在的三个所有权制度之一。这是 Rust 最独特和最令人信服的一个特点,其中 Rust 开发人员应该相当熟悉。所有权即 Rust 如何实现其最大目标和内存安全。这里有几个不同的概念,每一个概念都有它自己的章节:
-
所有权,即正在读的这篇文章。
-
借用,和与它们相关的功能‘引用’
- 生存期,借用的先进理念
这三篇文章相关且有序。如果你想完全的理解所有权制度,你将需要这三篇文章。
元
在我们了解细节之前,这里有关于所有权制度的两个重要说明需要知道。
Rust 注重安全和速度。它通过许多‘零成本抽象’来完成这些目标,这意味着在 Rust 中,用尽可能少的抽象成本来保证它们正常工作。所有权制度是一个零成本抽象概念的一个主要例子。我们将在这篇指南中提到的所有分析都是在编译时完成的。你不用为了任何功能花费任何运行成本。
然而,这一制度确实需要一定的成本:学习曲线。许多新用户使用我们喜欢称之为‘与借检查器的人斗争’,即 Rust 编译器拒绝编译那些作者认为有效的程序的 Rust 经验。这往往因为程序员对于所有权的怎样工作与 Rust 实现的规则不相符的心理模型而经常出现。你可能在第一次时会经历类似的事情。然而有个好消息:更有经验的 Rust 开发者报告称,一旦他们遵从所有权制度的规则工作一段时间后,他们会越来越少的与借检查器的行为斗争。
学习了这些后,让我们来了解借用。
借用
在所有权章节的末尾部分,我们有一个令人讨厌的功能,如下所示:
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,然而,由于它不能利用借出,以下是第一步:
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with v1 and v2
// return the answer
42
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let answer = foo(&v1, &v2);
// we can use v1 and v2 here!
我们使用了一个引用:&Vec<i32>,而不是将 Vec<i32> 作为我们的参数。同时,我们不是直接传递 V1 和 V2,我们传递 &V1 和 &V2。我们称 &T 为一个‘引用’,它借用了所有权,而不是拥有了资源。借用东西的绑定当它超出作用域时,它不解除分配给它的资源。这意味着在调用 foo() 函数后,我们原始的绑定可以再次被使用。
引用与绑定类似,它们都是不可变的。这意味着,在 foo() 函数中,向量根本不能改变:
fn foo(v: &Vec<i32>) {
v.push(5);
}
let v = vec![];
foo(&v);
错误:
error: cannot borrow immutable borrowed content `*v` as mutable
v.push(5);
^
推入一个值改变了这个变量,所以我们不允许这样做。
&mut引用
这里有第二种引用:&mut T。一个‘可变引用’允许你改变你借用的资源。例如:
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x);
以上代码将输出 6。我们将 y 标记为 x 的一个可变引用,然后将 y 指向的内容加 1。你将会注意到 x 也不得不被标记为 mut,如果不被标记,我们不能将一个可变值借用给一个不可变值。
其他方面,&mut 引用与其他引用一样。尽管它们之间以及它们如何相互作用有一个很大的区别。你可以在上面的例子中看到一些可疑的东西,因为我们需要那个用 { 和 } 包围的额外空间。如果我们删除它们,我们将会得到一个错误:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow ends
let y = &mut x;
^
note: previous borrow ends here
fn main() {
}
^
事实证明,这里是有规则的。
规则
这里有关于借用在 Rust 中的规则:
首先,任何借用必须持续比所有者的作用域小。其次,你可能有一个或者两种其他的借用,但是两种不能在同一时间同时使用:
-
一个资源的从 0 到 N 的引用 ( &T )。
- 一个可变的引用 ( &mut T )
你可能会注意到这与数据竞争的定义非常相似,尽管并不是完全相同:
There is a ‘data race’ when two or more pointers access the same memory location at the same time, where at least one of them is writing, and the operations are not synchronized.
使用引用,由于它们都不编写,所以你喜欢用多少用多少。如果你想要编写,你需要两个或者更多的指针指向同一内存,你每次仅仅可以使用一个 &mut。这就是 Rust 如何在编译时避免数据竞争:如果我们违反了规则,我们将会得到错误。
学习了以上内容后,让我们再次重新考虑我们的例子。
在作用域中的思考
以下为相关代码:
let mut x = 5;
let y = &mut x;
*y += 1;
println!("{}", x);
这些代码给出我们如下错误:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
这是因为我们违反了规则,我们有一个 &mut T 指向 x ,所以我们不允许创建任何 &T。反之亦然。以下注释暗示我们如何思考这个问题:
note: previous borrow ends here
fn main() {
}
^
换句话说,这个可变的借用贯穿了我们剩余的示例。我们想要的是在我们尝试调用 printLn! 之前,可变借用结束,然后使用不变的借用。在 Rust 中,借用被绑定在对于借用有效的作用域内。我们的作用域如下所示:
let mut x = 5;
let y = &mut x;// -+ &mut borrow of x starts here
// |
*y += 1; // |
// |
println!("{}", x); // -+ - try to borrow x here
// -+ &mut borrow of x ends here
作用域冲突:我们不能让 &x 和 y 在一个作用域内。
因此我们添加大括号:
let mut x = 5;
{
let y = &mut x; // -+ &mut borrow starts here
*y += 1;// |
} // -+ ... and ends here
println!("{}", x); // <- try to borrow x here
此时没有问题。我们可变的借用在我们创建不变的借用之前,超出了作用域。但是作用域是我们可以观察到一个借用可以持续多久的关键。
借用问题预防
为什么会有这些预防性的规则?正如我们所指出的,这些规则预防了数据竞争现象。什么样的问题是引起数据竞争现象的原因呢?以下有几个原因。
迭代器失效
其中一个例子是当你尝试改变你正在循环的集合时将会出现的‘迭代器失效’。Rust 的借用检查器防止这种情况的发生:
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
}
以上代码将打印出从 1 到 3 的数字。当我们循环访问这些向量时,我们仅仅给出了这些元素的引用。V 本身作为不可变的借用,这意味着,在我们遍历的过程中我们不能改变它:
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
v.push(34);
}
以下是出现的错误:
error: cannot borrow `v` as mutable because it is also borrowed as immutable
v.push(34);
^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
^
note: previous borrow ends here
for i in &v {
println!(“{}”, i);
v.push(34);
}
^
我们不能修改 V,因为它在循环中被借用。
释放内存后再使用
引用必须与它们引用到的资源存活时间一样长。 Rust 会检查你的引用的作用域来保证它为真。
如果 Rust 不检查这个属性,我们可能会无意间使用一个无效的引用。例如:
let y: &i32;
{
let x = 5;
y = &x;
}
println!("{}", y);
我们将会得到以下错误:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
{
let x = 5;
y = &x;
}
note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}
换句话说,y 只在 X 存在的作用域内有效。一旦 x 消失了,它将会变成一个 x 的无效引用。因此,上面代码中的错误中说借用‘活的时间不够长’,因为它在有效的矢量的时间内是无效的。
当在引用到的变量之前声明引用时,会发生同样的问题:
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
我们会得到以下错误:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
}
note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
let x = 5;
y = &x;
println!("{}", y);
}