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);
# }

实现有所滑移,这就是为什么第一个例子也能编译。滑移到什么程度很难说,因为不仅没有规范,这个功能似乎甚至没有文档 🤷。

最后修改 August 23, 2026: 更新 (499855b16)