2.8 理解函数内的借用
译文 · 基于 Learning Rust
理解函数内的借用
原文链接: https://quinedot.github.io/rust-learning/lifetime-analysis.html
编译器为确定生命周期和借用检查而进行的函数体内分析相当复杂。全面探索超出了本指南的范围,但这里我们会做一个简要介绍。
如果你遇到无法理解的错误,最好的办法是在论坛或其他地方寻求帮助。
函数体内的借用错误
以下是函数体内借用检查错误的一些简单原因。
回顾基础
需要牢记的最基本机制是:&mut 引用是独占的,而 & 引用是共享的并实现 Copy。你不能混用对同一值的共享引用和独占引用,也不能混用对同一值的两个独占引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # fn main() {
let mut local = "Hello".to_string();
// 创建并使用共享引用
let x = &local;
println!("{x}");
// 创建并使用独占引用
let y = &mut local;
y.push_str(", world!");
// 尝试再次使用共享引用
println!("{x}");
# }
|
这无法编译,因为一旦你创建了独占引用,任何其他现有引用都必须失效。
借用常常是隐式的
这是同样的例子,只是稍作改写。
1
2
3
4
5
6
7
8
9
10
11
12
13
| # fn main() {
let mut local = "Hello".to_string();
// 创建并使用共享引用
let x = &local;
println!("{x}");
// 隐式创建并使用独占引用
local.push_str(", world!");
// 尝试再次使用共享引用
println!("{x}");
# }
|
这里,push_str 接受 &mut self,
因此作为方法调用的一部分,存在隐式的 &mut local,所以这个例子仍然无法编译。
创建 &mut 不是唯一的独占使用
借用检查器会查看值的每一次使用,以判断它是否与对该值的借用的生命周期兼容,而不仅仅是涉及引用的使用,也不仅仅是涉及生命周期的使用。
例如,移动一个值会使对该值的任何引用失效,否则那些引用就会悬垂。
1
2
3
4
5
6
7
8
9
10
11
12
13
| # fn main() {
let local = "Hello".to_string();
// 创建并使用共享引用
let x = &local;
println!("{x}");
// 移动该值
let _local = local;
// 尝试再次使用共享引用
println!("{x}");
# }
|
被引用的值必须保持在作用域内
值离开作用域的效果类似于移动该值:所有引用都会失效。
1
2
3
4
5
6
7
8
9
10
| # fn main() {
let x;
{
let local = "Hello".to_string();
x = &local;
} // `local` 在此处离开作用域
// 在 `local` 离开作用域后尝试使用共享引用
println!("{x}");
# }
|
使用 &mut self 或 &self 算作对所有字段的使用
在下面的例子中,当我们创建 &self 来调用 bar 时,left 变得无效。因为你可以从 &self 得到 &self.left,这类似于试图混用 &mut self.left 和 &self.left。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #[derive(Debug)]
struct Pair {
left: String,
right: String,
}
impl Pair {
fn foo(&mut self) {
let left = &mut self.left;
left.push_str("hi");
self.bar();
println!("{left}");
}
fn bar(&self) {
println!("{self:?}");
}
}
|
更一般地说,创建 &mut x 或 &x 算作对从 x 可达的一切的使用。
一些能成功编译的情况
一旦你开始掌握借用错误,你可能会开始疑惑为什么某些程序被允许编译。这里我们介绍 Rust 如何在仍然保持健全性的同时允许非平凡的借用。
独立借用字段
Rust 单独跟踪结构体字段的借用,因此下面 left 和 right 的借用不会冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # #[derive(Debug)]
# struct Pair {
# left: String,
# right: String,
# }
#
impl Pair {
fn foo(&mut self) {
let left = &mut self.left;
let right = &mut self.right;
left.push_str("hi");
right.push_str("there");
println!("{left} {right}");
}
}
|
这种能力也称为拆分借用(splitting borrows)。
注意,通过索引访问的数据不被视为字段本身;相反,索引是一种通常借用整个 &self 或 &mut self 的操作。
1
2
3
4
5
6
7
8
9
10
| # fn main() {
let mut v = vec![0, 1, 2];
// 这两个不重叠,但是……
let left = &mut v[..1];
let right = &mut v[1..];
// ……借用检查器无法识别这一点
println!("{left:?} {right:?}");
# }
|
通常在这种情况下,会使用像 split_at_mut 这样的方法来拆分借用。
与索引类似,当你通过"解引用强制转换"访问某些东西时,你正在使用 Deref trait(或 DerefMut),它会借用整个 self。
然而,也有一些特殊情况下借用检查器更聪明。
1
2
3
4
5
6
7
8
9
10
11
12
| # fn main() {
// 模式匹配理解不重叠的切片(切片是特殊的)
let mut v = vec![String::new(), String::new()];
let slice = &mut v[..];
if let [_left, right] = slice {
if let [left, ..] = slice {
left.push_str("left");
}
// 仍然可用!
right.push_str("right");
}
# }
|
1
2
3
4
5
6
7
8
| # fn main() {
// 你可以通过 `Box` 解引用拆分借用(`Box` 是特殊的)
let mut bx = Box::new((0, 1));
let left = &mut bx.0;
let right = &mut bx.1;
*left += 1;
*right += 1;
# }
|
这些示例并不详尽 🙂。
重借用
如前所述, 重借用是让 &mut 合理可用的关键。事实上,它们具有你无法用自定义结构体和 trait 实现来模拟的其他特殊属性。考虑这个例子:
1
2
3
| fn foo(s: &mut String) -> &str {
&**s
}
|
实际上,这太快了。让我们稍微改一下,逐步来看。
1
2
3
4
5
| fn foo(s: &mut String) -> &str {
let ms: &mut str = &mut **s;
let rs: &str = &*s;
rs
}
|
这里,s 和 ms 都在 foo 结束时离开作用域,但这并不会使 rs 失效。也就是说,通过引用进行重借用可以对重借用施加生命周期约束,但重借用并不依赖于引用保持在作用域内!它只依赖于被借用的数据。
这表明重借用比嵌套引用更强大。
共享重借用
在检测冲突时,借用检查器区分共享重借用和独占重借用。特别是,创建共享重借用会使同一值的任何独占重借用失效(因为它们不再独占)。但它不会使共享重借用失效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| struct Pair {
left: String,
right: String,
}
impl Pair {
fn foo(&mut self) {
// 拆分借用:独占重借用,共享重借用
let left = &mut self.left;
let right = &self.right;
left.push('x');
// 对整个 `self` 的共享重借用,"覆盖"所有字段
let this = &*self;
// 它使任何独占重借用失效,所以这会失败……
// println!("{left}");
// 但它不会使共享重借用失效!
println!("{right}");
}
}
|
两阶段借用
以下代码可以编译:
1
2
3
4
5
| # fn main() {
let mut v = vec![0];
let shared = &v;
v.push(shared.len());
# }
|
然而,如果你了解这里的求值顺序,它似乎不应该能编译。隐式的 &mut v 应该在 shared.len() 被求值之前就使 shared 失效。怎么回事?
这是称为两阶段借用(two-phase borrows)的特性造成的结果,旨在让嵌套方法调用更符合人体工程学:
1
2
3
4
| # fn main() {
let mut v = vec![0];
v.push(v.len());
# }
|
在远古时代, 你必须这样写:
1
2
3
4
5
| # fn main() {
let mut v = vec![0];
let len = v.len();
v.push(len);
# }
|
实现有所滑移,这就是为什么第一个例子也能编译。滑移到什么程度很难说,因为不仅没有规范,这个功能似乎甚至没有文档 🤷。