近年来,Rust已经迅速成为最流行和增长最快的编程语言之一。谷歌和微软等大型科技公司正在使用和投资它。它是一种允许带有特殊约束的手动内存管理的语言,这在很大程度上确保了内存安全。
然而,Rust使用的约束(通常称为借用检查器)可能非常难以学习。嗯,如果你没有正确学习的话。
这篇文章可以使你快速学习Rust中正确的引用概念。前提是你有一些Rust的基础知识,比如结构体、函数和向量。
什么是引用?
引用是指在不显式复制的情况下引用某些数据或变量的方法。Rust的引用与C和C++中的非混淆指针相同。在C和C++中,非混淆指针都是用restrict关键字定义的。在Rust中,引用采用的正是这种行为。但是,任何使引用相互命名别名的尝试,无论是使用unsafe块还是使用Rust的指针(这是另一个主题),都将导致未定义的行为。不要这样做。
在Rust中,有四种方法可以将变量“传递”或转移到函数或作用域之外。
1,移动变量:默认情况下,Rust会在赋值或从函数返回值时移动值。移动意味着一旦变量被移动,就不能在之前的位置使用它。
2,传递不可变引用:不可变引用是一种从另一个作用域引用变量的方法,只要该引用不会超出它所引用的变量的作用域。在Rust中,这被称为生命周期。可以有一个或多个对变量的不可变引用。
3,传递可变引用:可变引用是引用来自另一个作用域的变量的一种方式,适用于类似的生命周期规则。但是,一个变量一次只有一个可变引用。这意味着在任何给定时间,任何变量都只能通过单个引用进行修改。
4,传递副本:在Rust中,不同的类型可以实现Copy或Clone特征,这样它们就可以隐式或显式地复制。Copy和Clone之间的主要区别在于前者是一个字节一个字节的memcpy风格复制,而Clone是显式实现的一个成员一个成员的复制,可以使用自定义逻辑。
规则
引用的第一个也是最重要的规则是只有一个可变引用或多个不可变引用。但有一个问题是,这在实践中看起来如何?让我们来看几个例子,从下面这个开始:
fn main() {
let mut a = 6;
let b = &a;
let c = &mut a;
println!("{}", *c);
}
上面的代码实际上是有效的,你可能会认为同时存在不可变引用和可变引用。然而,需要注意的是,代码只使用了c,没有使用b下的不可变引用。由于这个原因,Rust的借用检查器不会报错。但是让我们看看当我们开始使用b时会发生什么:
fn main() {
let mut a = 6;
let b = &a;
let c = &mut a;
println!("{}", *b);
}
这会导致编译失败:
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
--> src/main.rs:7:13
|
6 | let b = &a;
| -- immutable borrow occurs here
7 | let c = &mut a;
| ^^^^^^ mutable borrow occurs here
8 | println!("{}", *b);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
b被println!借走了,这会导致不可变和可变引用不能同时存在的规则被打破。
接下来,让我们看一个更复杂的例子:
fn main() {
let mut a = 6;
let mut b = &a;
let c = &mut b;
println!("{}", *c);
}
乍一看,这看起来像是对同一个变量取了一个可变引用和一个不可变引用。然而,理解引用既是类型又是操作符是至关重要的。当使用引用操作符时,它接受与该操作符一起使用的变量的引用。
这意味着,c是对整数引用的可变引用。这个引用的Rust类型看起来像&mut&usize。在上面的代码中,c可以被解引用并指向一个不同的&usize引用,这个引用会改变b,但不会改变a。如果我们试图通过c来改变a,如下:
fn main() {
let mut a = 6;
let mut b = &a;
let c = &mut b;
**c += 1;
println!("{}", *c);
}
会出现以下错误:
error[E0594]: cannot assign to `**c`, which is behind a `&` reference
--> src/main.rs:8:5
|
8 | **c += 1;
| ^^^^^^^^ cannot assign
引用,类似于C/C++中的指针,可以形成任意长度的复合类型,这样,&mut&mut&usize也可以作为Rust引用存在。与指针不同的是,引用的生命周期必须足够长,否则,借用检查器会让你止步不前。
生命周期
在这里,我们可以探索各种引用的生命周期,并了解何时创建和销毁引用(或者像Rust所说的“drop”)。下面的例子:
fn main() {
let mut a = 6;
let mut b = &a;
{
let c = 7;
b = &c;
}
println!("{}", *b);
}
产生错误:
error[E0597]: `c` does not live long enough
--> src/main.rs:9:13
|
8 | let c = 7;
| - binding `c` declared here
9 | b = &c;
| ^^ borrowed value does not live long enough
10 | }
| - `c` dropped here while still borrowed
11 | println!("{}", *b);
| -- borrow later used here
在内部作用域中,b被改变为保存对c的引用。但是一旦内部作用域结束,c就不存在了。因此,在这种情况下,引用比它引用的变量生命周期更长,所以产生了错误。
同样的规则不适用于副本,因为副本是彼此独立存在的。如果采用相同的代码来删除引用的使用:
fn main() {
let mut a = 6;
let mut b = a;
{
let c = 7;
b = c;
}
println!("{}", b);
}
代码编译没有错误。由于整数相对较小,因此通常可以复制它们。然而,更大的类型使用引用计数或按引用传递,以避免性能下降。
基于作用域的生命周期规则也适用于在较大的类实例中获取引用。
struct Container(Vec<u64>);
impl Container {
fn get(&self, index:usize) -> &u64 {
&self.0[index]
}
}
在上面的代码中,get返回对vector中的引用,但是vector的生命周期必须比返回的引用长。如果我们应用同样的逻辑,
fn main() {
let m = Container(vec![1, 2, 3]);
let mut the_ref = m.get(0);
{
let d = Container(vec![1, 2, 3]);
the_ref = d.get(1);
}
println!("{}", the_ref);
}
此代码也无法编译,并出现类似的错误
error[E0597]: `d` does not live long enough
--> src/main.rs:15:19
|
14 | let d = Container(vec![1, 2, 3]);
| - binding `d` declared here
15 | the_ref = d.get(1);
| ^ borrowed value does not live long enough
16 | }
| - `d` dropped here while still borrowed
17 | println!("{}", the_ref);
| ------- borrow later used here
当某些东西在Rust中被删除时,所有实现Drop特性的成员也将被删除。
迭代和引用
当在迭代或循环中使用引用时,有几种独特的行为。如果迭代也是不可变的,则对集合类型的迭代,通常使循环充当该集合上的不可变借用的作用域。以下代码为例:
fn main() {
let mut a = vec![1, 2, 3, 4];
for elem in a.iter() {
if *elem % 2 == 0 {
a.remove(*elem);
}
}
}
会导致编译错误:
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
--> src/main.rs:8:13
|
6 | for elem in a.iter() {
| --------
| |
| immutable borrow occurs here
| immutable borrow later used here
7 | if *elem % 2 == 0 {
8 | a.remove(*elem);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
Rust遵循这样的规则:对某种类型的不可变迭代是一系列不可变借用,因此,不能在该迭代期间可变地借用相同的类型。
现在,你可能会认为这段特定代码的解决方案是对其进行可变迭代。然而,这仍然是不正确的!如果将iter()改为iter_mut():
fn main() {
let mut a = vec![1, 2, 3, 4];
for elem in a.iter_mut() {
if *elem % 2 == 0 {
a.remove(*elem);
}
}
}
会出现以下错误:
error[E0499]: cannot borrow `a` as mutable more than once at a time
--> src/main.rs:8:13
|
6 | for elem in a.iter_mut() {
| ------------
| |
| first mutable borrow occurs here
| first borrow later used here
7 | if *elem % 2 == 0 {
8 | a.remove(*elem);
| ^ second mutable borrow occurs here
让我们回顾一下引用规则,一个或多个不可变引用,或者仅仅是一个可变引用。在本例中,我们创建了两个可变引用,借用检查器将拒绝它们。但是这个规则实际上是有意义的,它可以保护免受内存损坏错误的影响。
根据集合的内部实现,修改集合类型会使现有迭代器失效。这可能是因为集合处理的内存块可能被分配或释放,从而导致悬空指针,但是可变引用规则有效地防止了这种情况。