4.4 引用与借用
4.4.0 写在正文之前
这一节的内容其实就相当于 C++ 的智能指针移动语义在编译器层面做了一些约束。Rust 中引用的写法,通过编译器的约束,变成了 C++ 中最理想、最规范的指针写法。所以学过 C++ 的人对这一章肯定会非常熟悉。
4.4.1 引用
引用让函数使用某个值而不获得其所有权。声明时在类型前加上 & 即代表引用。例如,String 的引用就是 &String。如果学过 C++,C++ 中的解引用运算符是 *,Rust 中也是一样的。
学了引用之后,就可以把上一篇文章 4.3. 所有权与函数 最后的示例简化。
这是先前的代码:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}
这是修改后的代码:
fn main() {
let s1 = String::from("hello");
let length = calculate_length(&s1);
println!("The length of '{}' is {}", s1, length);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
对比两者,后者把指向数据的指针传入 calculate_length 函数供其操作,而数据所有权依然在变量 s1 上。不需要返回元组,也不需要再声明一个变量 s2,因此更加简洁。
函数 calculate_length 的参数 s 实际上是一个指针,指向 s1 所在的栈内存位置(不会直接指向堆内存中的数据)。当这个指针走出作用域时,Rust 并不会销毁它所指向的数据,因为 s 并不拥有它。Rust 只会弹出栈上存储的指针信息,也就是释放下图中最左侧部分所占的内存。

以引用作为函数参数叫做借用。
4.4.2 借用的特性
借用的内容不能被修改,除非是可变引用。
以房产为例:你把自己有所有权的房子租给别人,就是借用。租户可以住,但不能随便装修;这就是借用内容不能被修改的特性。如果你允许租客装修,那就是可变引用。
以这段代码为例:
fn main() {
let s1 = String::from("hello");
let length = calculate_length(&s1);
println!("The length of '{}' is {}", s1, length);
}
fn calculate_length(s: &String) -> usize {
s.push_str(", world");
s.len()
}
这段代码在编译时会报错:
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | s.push_str(", world");
| ^ `s` is a `&` reference, so it cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn calculate_length(s: &mut String) -> usize {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
报错原因在于 s.push_str(", world"); 这一行:引用默认是不可变的,但这一行修改了数据。
引用跟普通变量声明一样,默认不可变,但加上 mut 关键字后就可变了:
fn main() {
let mut s1 = String::from("hello");
let length = calculate_length(&mut s1);
println!("The length of '{}' is {}", s1, length);
}
fn calculate_length(s: &mut String) -> usize {
s.push_str(", world");
s.len()
}
这样写就不会报错了,但记得在声明 s1 时把它声明为可变变量。
这种可以修改数据的引用叫做可变引用。
4.4.3 可变引用的限制
可变引用有两个非常重要的限制。第一个是:在任意给定时刻,对某一块数据,只能有一个可变引用。
以这段代码为例:
fn main() {
let mut s = String::from("hello");
let s1 = &mut s;
let s2 = &mut s;
println!("{}, {}", s1, s2);
}
因为 s1 和 s2 都是指向 s 的可变引用,且同时被使用,所以编译器会报错:
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:4:14
|
3 | let s1 = &mut s;
| ------ first mutable borrow occurs here
4 | let s2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 |
6 | println!("{}, {}", s1, s2);
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `mutable-ref` (bin "mutable-ref") due to 1 previous error
这么做的目的是防止数据竞争。当以下三个条件同时满足时,就会发生数据竞争:
- 两个或多个指针同时访问同一数据
- 至少一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
报错信息中提到了 at a time,意思是同时——也就是前一个借用仍在被使用的期间。所以只要它们不重叠,两个可变引用在不同作用域中指向同一块数据是允许的。下面的代码就体现了这一点:
fn main() {
let mut s = String::from("hello");
{
let s1 = &mut s;
}
let s2 = &mut s;
}
s1 和 s2 的作用域不相同,所以指向同一块数据是允许的。
可变引用的第二个重要限制是:不可以同时拥有一个可变引用和一个不可变引用。 可变引用的目的是修改数据,而不可变引用的目的是保持数据不变。如果两者同时存在,一旦可变引用改变了值,不可变引用就失去了作用。
fn main() {
let mut s = String::from("hello");
let s1 = &mut s;
let s2 = &s;
println!("{}, {}", s1, s2);
}
因为 s1 是可变引用,s2 是不可变引用,两者同时指向同一块数据并被使用,所以编译器会报错:
error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
--> src/main.rs:4:14
|
3 | let s1 = &mut s;
| ------ mutable borrow occurs here
4 | let s2 = &s;
| ^^ immutable borrow occurs here
5 |
6 | println!("{}, {}", s1, s2);
| -- mutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `mixed-ref` (bin "mixed-ref") due to 1 previous error
当然,多个不可变引用可以同时存在。
总结:多个读者(不可变引用)可以同时存在,多个写者(可变引用)可以存在但不能同时,多个写者以及同时读写是不允许的。
4.4.4 悬空引用
在使用指针时,很容易引出叫做悬空指针的错误。其定义为:一个指针引用了内存中的某个地址,而这块内存可能已经被释放并重新分配给其他人使用了。
如果你引用了某些数据,Rust 的编译器保证在引用离开作用域之前,数据不会离开作用域。 这就是 Rust 确保悬空引用永远不会出现的方式。
以这段代码为例:
fn main() {
let r = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
- 创建了一个局部变量
s: 变量s是一个String。它被分配在栈上,但其底层数据存储在堆上。 - 返回对
s的引用: 函数最后通过&s返回了s的引用。 s离开作用域: 函数dangle返回后,变量s离开作用域。根据 Rust 的所有权规则,s的内存会被自动释放。&s所指向的内存数据已不再存储s的数据,因此返回的引用指向的是已经被释放的内存地址,变成了悬空引用。
Rust 的编译器会检查到这一点,并在编译时报错。
4.4.5 引用的规则
- 在任何给定时刻,只能满足下列条件之一:
- 一个可变引用
- 任意数量的不可变引用
- 引用必须一直有效