借用检查器如何确定 `&mut` 的生命周期
Source: Dev.to
请提供您希望翻译的正文内容,我将为您翻译成简体中文并保留原有的格式。
介绍
在本文中,我们将分析 Rust 的 borrow checker 如何在存在 非词法生命周期(Non‑Lexical Lifetimes,NLL)的情况下确定可变借用 (&mut) 的实际持续时间。
具体来说,我们将看到为什么借用的持续时间并不等同于其词法持续时间,以及为什么显式调用 drop() 不能 在控制流中仍有后续使用时手动强制结束借用。
Source:
技术问题
在学习 Rust Book 第 4.1 – What is Ownership 章节以及 Rustlings 中的练习 06_move_semantics/move_semantics4.rs 时,一个看似简单的程序试图对 x 创建两个连续的可变借用,却因 E0499 错误而编译失败。
编译器给出的信息:
A variable was borrowed as mutable more than once.
Erroneous code example:
let mut i = 0;
let mut x = &mut i;
let mut a = &mut i;
x;
// error: cannot borrow `i` as mutable more than once at a time
Please note that in Rust, you can either have many immutable references, or one mutable reference.
出错的代码如下:
#[cfg(test)]
mod tests {
// TODO: Fix the compiler errors only by reordering the lines in the test.
// Don't add, change or remove any line.
#[test]
fn move_semantics4() {
let mut x = Vec::new();
let y = &mut x;
let z = &mut x;
y.push(42);
z.push(13);
assert_eq!(x, [42, 13]);
}
}
正如我们所见,在第一次借用仍然(间接)需要时就尝试了第二个 &mut x。
Rust 的所有权规则 要求同一时刻只能有一个活跃的可变引用 (&mut)。
修复
修复非常简单:只需把第二次创建借用的语句移动到第一次借用的最后一次使用之后。
#[cfg(test)]
mod tests {
#[test]
fn move_semantics4() {
let mut x = Vec::new();
let y = &mut x;
y.push(42);
let z = &mut x;
z.push(13);
assert_eq!(x, [42, 13]);
}
}
为什么可行
let y = &mut x;— 编译器记录 存在一个名为y的活跃可变借用。y.push(42);— 这是对y的最后一次使用。此行之后,对编译器而言x上已经没有活跃的可变借用,因此let z = &mut x;成为合法。
这种行为得益于 非词法生命周期 (Non‑Lexical Lifetimes, NLL):可变借用在最后一次使用结束时结束,而不一定要等到块的结尾。
问题
有没有办法显式声明
y的生命周期,使得编译器不使用 NLL,而是通过代码由我来指明y的存续时间?
回答
不可以。不能在局部变量上声明显式的生命周期来强制借用检查器忽略 NLL。
生命周期参数用于为类型、函数或结构签名,而不是直接控制函数内部绑定的词法作用域持续时间。
drop() 与借用的生命周期
考虑到 lifetimes(生命周期),我们可以尝试使用 drop() 在“最后一次使用”之前结束一个借用。
fn main() {
let mut x = Vec::new();
let y = &mut x;
y.push(42);
drop(y);
let z = &mut x; // ← 这里出现 E0499:y 的借用仍然存活
y.push(43); // ← 这里出现 E0382:在被 drop(y) 移动后使用 y
z.push(13);
}
编译器会给出两个不同的错误:
- E0499 –
cannot borrow 'x' as mutable more than once at a time
第二个&mut x在第一个借用仍被视为活跃时声明。 - E0382 –
use of moved value 'y'
y已经被drop(y)移动,随后又被使用。
为什么 drop() 不会缩短可变借用的生命周期
- NLL(非词法生命周期)基于 MIR(中间表示)并使用活跃性分析,将借用的生命周期延长到在控制流上能够识别的最后一次使用。
drop(y)只是一次移动:它使绑定y不再可用,但不会在编译器检测到后续对y的使用时缩短借用的区域。
实际上,drop(y) 只会在绑定在 drop 之后不再被使用的情况下结束借用。如果还有后续使用,NLL 会把借用的生命周期向后延伸到那次使用,使得 drop 对借用检查没有影响。
实践原则
确保第一个 &mut 的 最后一次使用 在创建新的可变借用之前 发生。如果需要提前“释放”借用,必须将代码结构化,使其后不再有使用,或者使用块 ({ … }) 来限制绑定的词法作用域。
在 NLL(非词法生命周期)下,编译器已经为你完成了大部分工作,但重要的是要明白 drop() 并不是缩短生命周期的魔法棒。
可以通过显式作用域、提取到函数或重新设计数据来管理后续 &mut 的创建,以避免不必要的可变别名。
你呢?是否曾遇到过类似的情况?
在评论中告诉我吧!
期待再见,
Mirko