学完所有权这一章,是不是感觉有点绕,定义一个值要考虑它怎么传来传去,函数传值返回要转移,绑定给其他变量也要转移。有没有一种方法,能让 Rust 像其他语言一样,只使用某个变量的指针或者是引用呢?答案就是:借用。

Rust 通过借用(borrowing)来获取变量的引用,借用可以让我们在不同的地方使用相同的数据,而不违反所有权规则

引用与解引用

常规引用是一个指针类型,指向对象存储的内存地址,不论数据存储在栈上还是堆上,引用都指向它的内存地址。

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
}

Rust 中使用 & 获取变量的引用,这属于不可变引用,当然也有可变引用 &mutlet y = &x 表示 yx 的一个引用,x 被绑定为 5,所以断言 assert_eq!(5, x);能通过。assert_eq!(5, *y);*y 表示 y 的解引用,也就是解出引用所指向的值,所以这个断言依旧能通过。然而如果 assert_eq!(5, y);.则会报错:

1
2
3
4
5
6
7
8
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` // 无法比较整数类型和引用类型
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for `{integer}`

因为它们是不同的类型,所以不允许比较。

不可变引用 &T

不可变引用允许你读取数据但不能修改它。可以同时拥有任意数量的不可变引用:

1
2
3
4
5
6
7
8
fn main() {
let x = 5;
let r1 = &x;
let r2 = &x;
println!("{} and {}", r1, r2);
// 这里可以同时使用 r1 和 r2,因为它们都不会修改 x
}

再来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

我们把 s1的引用传给函数,函数返回字符串长度。

如果不使用引用,将会是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
let s1 = String::from("hello");

let len = calculate_length(s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: String) -> usize {
s.len()
}
//这样就会报错,因为s1的所有权已经被转移走了,println!访问s1会报错

//下面代码就不会报错
fn main() {
let s1 = String::from("hello");

let len = calculate_length(s1.clone());

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: String) -> usize {
s.len()
}

需要给函数传入 s1 的深拷贝s1.clone(),保留s1 的所有权,这样就不会报错了。可以看到引用起码解决了两个问题:

  1. 代码更加简洁;
  2. 不用再开辟内存空间,优化了性能。

由于可以同时存在多个不可变引用,所以不可变引用一定是不能修改的:

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = String::from("hello");

change(&s);
}

fn change(some_string: &String) {
some_string.push_str(", world");
}

会导致报错:

1
2
3
4
5
6
7
8
9
10
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
`some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改

同变量存在可变与不可变一样,也存在可变引用。

可变引用 &mut T

可变引用允许修改引用的数据,在同一作用域中,对于一个特定数据只能存在一个可变引用。很容易理解,如果存在多个可变引用,那会发生数据竞争呀!

1
2
3
4
5
6
7
8
fn main() {
let mut x = 5;
let r1 = &mut x;
*r1 += 1;
println!("{}", x);
// 此处通过可变引用 r1 修改了 x,且在修改期间不能有其他引用
}

所以对于上一节的例子,改为可变引用即可:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut s = String::from("hello");

change(&mut s);
println!("s = {}", s) // s = hello, world
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

同一作用域只能存在一个可变引用

看下面例子:

1
2
3
4
5
6
7
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

很明显 r1r2 都处于同一作用域,所以会报错:

1
2
3
4
5
6
7
8
9
10
11
error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间无法对 `s` 进行两次可变借用
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here 首个可变引用在这里借用
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here 第二个可变引用在这里借用
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here 第一个借用在这里使用

阿R真的很严格,他在编译期就杜绝了数据竞争这个问题,以下行为可能会导致数据竞争:

  • 两个或更多的指针同时访问同一数据
  • 至少有一个指针被用来写入数据
  • 没有同步数据访问的机制

数据竞争会导致未定义行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!

可变引用与不可变引用不能同时存在

以下代码会导致错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
// 无法借用可变 `s` 因为它已经被借用了不可变
--> src/main.rs:6:14

let r1 = &s; // 没问题
-- immutable borrow occurs here 不可变借用发生在这里
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
^^^^^^ mutable borrow occurs here 可变借用发生在这里

println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here 不可变借用在这里使用

这是肯定的,想想你使用的不可变引用,在另外一个地方被改变了,可能会导致未知的问题。而多个不可变引用能同时存在则是大家都只能读取数据而不是修改,自然不用担心数据会被污染。

此外,还有很重要的一点:

引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

Rust 的编译器一直在优化,早期的时候,引用的作用域跟变量作用域是一致的,这对日常使用带来了很大的困扰,你必须非常小心的去安排可变、不可变变量的借用,免得无法通过编译,例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中,r1,r2作用域在这里结束

let r3 = &mut s;
println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束

在老版本的编译器中(Rust 1.31 前),将会报错,因为 r1r2 的作用域在花括号 } 处结束,那么 r3 的借用就会触发 无法同时借用可变和不可变的规则。

但是在新的编译器中,该代码将顺利通过,因为 引用作用域的结束位置从花括号变成最后一次使用的位置,因此 r1 借用和 r2 借用在 println! 后,就结束了,此时 r3 可以顺利借用到可变引用。

非词法作用域生命周期(NLL)

像这样的编译器特性,有一个专门的名字: Non-Lexical Lifetimes(NLL) 。中文叫作非词法作用域生命周期。Rust 2018 Edition 引入了 NLL,这是一种更灵活的借用检查器,允许引用在实际不再使用后立即被回收,而不是在其词法作用域结束时。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut x = 5;
{
let r1 = &mut x;
*r1 += 1;
} // 在 NLL 之前,x 在这里仍然被 r1 借用。NLL 允许在这一点后立即重新借用。
let r2 = &mut x;
*r2 += 1;
println!("{}", x);
}

所以牢记,在引用被使用之后才能再次定义可变引用。

悬垂引用

悬垂引用,也叫悬垂指针,是指向无效内存的指针。比如指针指向某个值之后,该值被释放了,而指针依然存在,指针指向的内存不存在任何值或已被其他变量重新使用。而在 Rust 中,编译器可以确保永远不会存在悬垂指针:当获取到数据的引用之后,编译器会确保在引用结束前数据不会被释放,要想释放数据,必须先停止其引用的使用。

例如:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let r = dangle();
// ...
}

// 错误示例:尝试返回一个悬垂引用
fn dangle() -> &String {
let s = String::from("hello");
&s // s 在离开 dangle 的作用域时被丢弃。它的引用不能返回给 main。
}

会导致报错:

1
2
3
4
5
6
7
8
9
10
11
12
error[E0106]: missing lifetime specifier
--> main.rs:7:16
|
7 | fn dangle() -> &String {
| ^ help: consider giving it a 'static lifetime: `&'static`
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

error: aborting due to previous error

For more information about this error, try `Rustc --explain E0106`.

1
2
3
this function's return type contains a borrowed value, but there is no value for it to be borrowed from.
该函数返回了一个借用的值,但是已经找不到它所借用值的来源

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String

解决办法很简单,返回 s 而不是返回它的引用。

1
2
3
4
5
6
7
8
9
10
fn main() {
let _r = dangle();
// ...
}

fn dangle() -> String {
let s = String::from("hello");
s
}

也就是把 s 的所有权转移给外面的调用者,而不是返回它的引用。

总结一下借用规则

同一时刻,只能拥有要么一个可变引用, 要么任意多个不可变引用

引用必须总是有效的

总结

Rust 的引用和借用规则是设计来保证代码的内存安全,避免数据竞争和悬垂指针等问题。通过限制引用的方式和作用域,Rust 编译器可以保证在编译时就避免这些常见的安全问题。