生命周期
Rust 通过*生命周期*来强制执行这些规则。生命周期是指代码中引用必须保持有效的命名区域。这些区域可能相当复杂,因为它们对应于程序中的执行路径。这些执行路径中甚至可能存在漏洞,因为只要在再次使用之前重新初始化引用,就可以使其失效。包含引用(或假装包含引用)的类型也可以标记生命周期,以便 Rust 也可以防止它们失效。
在我们的大多数示例中,生命周期将与作用域一致。这是因为我们的示例很简单。下面将描述它们不一致的更复杂情况。
在函数体内,Rust 通常不允许您显式命名所涉及的生命周期。这是因为在本地上下文中通常没有必要讨论生命周期;Rust 拥有所有信息,并且可以尽可能优化地处理所有事情。许多您原本需要编写的匿名作用域和临时变量通常会被引入,以使您的代码“正常工作”。
但是,一旦您跨越函数边界,就需要开始讨论生命周期。生命周期用撇号表示:`'a`、`'static`。为了初步了解生命周期,我们将假设我们实际上被允许使用生命周期标记作用域,并从本章开头开始对示例进行反糖化。
最初,我们的示例在作用域和生命周期周围使用了*积极的*语法糖——甚至是高果糖玉米糖浆——因为显式地写出所有内容会*非常冗长*。所有 Rust 代码都依赖于对“明显”事物的积极推断和省略。
一个特别有趣的语法糖是每个 `let` 语句都隐式地引入了一个作用域。在大多数情况下,这并不重要。但是,对于相互引用的变量来说,这很重要。举一个简单的例子,让我们完全反糖化这段简单的 Rust 代码
#![allow(unused)] fn main() { let x = 0; let y = &x; let z = &y; }
借用检查器总是试图最小化生命周期的范围,因此它很可能会反糖化为以下内容
// NOTE: `'a: {` and `&'b x` is not valid syntax!
'a: {
let x: i32 = 0;
'b: {
// lifetime used is 'b because that's good enough.
let y: &'b i32 = &'b x;
'c: {
// ditto on 'c
let z: &'c &'b i32 = &'c y; // "a reference to a reference to an i32" (with lifetimes annotated)
}
}
}
哇。这……太可怕了。让我们都花点时间感谢 Rust 让这一切变得更轻松。
实际上传递对外部作用域的引用将导致 Rust 推断出更大的生命周期
#![allow(unused)] fn main() { let x = 0; let z; let y = &x; z = y; }
'a: {
let x: i32 = 0;
'b: {
let z: &'b i32;
'c: {
// Must use 'b here because the reference to x is
// being passed to the scope 'b.
let y: &'b i32 = &'b x;
z = y;
}
}
}
示例:生命周期超过被引用者的引用
好了,让我们看看之前的一些例子
#![allow(unused)] fn main() { fn as_str(data: &u32) -> &str { let s = format!("{}", data); &s } }
反糖化为
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s;
}
}
`as_str` 的此签名接受对具有*某个*生命周期的 u32 的引用,并承诺它可以生成对可以存活*同样长*的 str 的引用。我们已经可以看到为什么这个签名可能会出现问题。这基本上意味着我们将在对 u32 的引用起源的作用域中或*更早*的某个地方找到一个 str。这有点强人所难。
然后我们继续计算字符串 `s`,并返回对它的引用。由于我们函数的约定规定引用必须比 `'a` 存活时间长,因此这就是我们为引用推断的生命周期。不幸的是,`s` 是在作用域 `'b` 中定义的,因此唯一合理的方法是 `'b` 包含 `'a`——这显然是错误的,因为 `'a` 必须包含函数调用本身。因此,我们创建了一个生命周期超过其被引用者的引用,这*实际上*是我们所说的引用不能做的第一件事。编译器理所当然地对我们大发雷霆。
为了更清楚地说明这一点,我们可以扩展这个例子
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s
}
}
fn main() {
'c: {
let x: u32 = 0;
'd: {
// An anonymous scope is introduced because the borrow does not
// need to last for the whole scope x is valid for. The return
// of as_str must find a str somewhere before this function
// call. Obviously not happening.
println!("{}", as_str::<'d>(&'d x));
}
}
}
糟糕!
当然,编写此函数的正确方法如下
#![allow(unused)] fn main() { fn to_string(data: &u32) -> String { format!("{}", data) } }
我们必须在函数内部生成一个拥有所有权的值才能返回它!我们唯一可以返回 `&'a str` 的情况是,如果它在 `&'a u32` 的字段中,这显然不是事实。
(实际上,我们也可以只返回一个字符串字面量,它作为一个全局变量可以被认为驻留在堆栈的底部;但这*稍微*限制了我们的实现。)
示例:为可变引用创建别名
另一个例子呢
#![allow(unused)] fn main() { let mut data = vec![1, 2, 3]; let x = &data[0]; data.push(4); println!("{}", x); }
'a: {
let mut data: Vec<i32> = vec![1, 2, 3];
'b: {
// 'b is as big as we need this borrow to be
// (just need to get to `println!`)
let x: &'b i32 = Index::index::<'b>(&'b data, 0);
'c: {
// Temporary scope because we don't need the
// &mut to last any longer.
Vec::push(&'c mut data, 4);
}
println!("{}", x);
}
}
这里的问题更加微妙和有趣。我们希望 Rust 因为以下原因拒绝此程序:当我们尝试获取对 `data` 的可变引用以进行 `push` 时,我们有一个对 `data` 的后代的活动共享引用 `x`。这将创建一个别名可变引用,这将违反引用的*第二条*规则。
然而,这*根本不是* Rust 推断此程序错误的原因。Rust 不理解 `x` 是对 `data` 的子路径的引用。它根本不理解 `Vec`。它*确实*看到的是,`x` 必须存活到 `'b` 才能被打印出来。`Index::index` 的签名随后要求我们对 `data` 的引用必须存活到 `'b`。当我们尝试调用 `push` 时,它会看到我们尝试创建一个 `&'c mut data`。Rust 知道 `'c` 包含在 `'b` 中,并且拒绝我们的程序,因为 `&'b data` 必须仍然存活!
在这里,我们看到生命周期系统比我们实际想要保留的引用语义要粗糙得多。在大多数情况下,*这完全没问题*,因为它让我们不必整天向编译器解释我们的程序。然而,这确实意味着,一些在 Rust 的*真正*语义方面完全正确的程序被拒绝了,因为生命周期太笨了。
生命周期覆盖的区域
引用(有时称为*借用*)从创建到最后一次使用都是*活动的*。被借用的值只需要比活动的借用存活时间长即可。这看起来很简单,但也有一些微妙之处。
以下代码段可以编译,因为在打印 x
后,它就不再需要了,所以它是否是悬垂指针或别名并不重要(即使变量 x
技术上 存在于作用域的最后)。
#![allow(unused)] fn main() { let mut data = vec![1, 2, 3]; let x = &data[0]; println!("{}", x); // This is OK, x is no longer needed data.push(4); }
但是,如果该值具有析构函数,则析构函数会在作用域结束时运行。而运行析构函数被认为是一种使用 ‒ 显然是最后一次使用。所以,这将不会编译。
#![allow(unused)] fn main() { #[derive(Debug)] struct X<'a>(&'a i32); impl Drop for X<'_> { fn drop(&mut self) {} } let mut data = vec![1, 2, 3]; let x = X(&data[0]); println!("{:?}", x); data.push(4); // Here, the destructor is run and therefore this'll fail to compile. }
一种让编译器相信 x
不再有效的方法是在 data.push(4)
之前使用 drop(x)
。
此外,借用可能有多个可能的最后使用位置,例如在条件语句的每个分支中。
#![allow(unused)] fn main() { fn some_condition() -> bool { true } let mut data = vec![1, 2, 3]; let x = &data[0]; if some_condition() { println!("{}", x); // This is the last use of `x` in this branch data.push(4); // So we can push here } else { // There's no use of `x` in here, so effectively the last use is the // creation of x at the top of the example. data.push(5); } }
并且一个生命周期中可能会有暂停。或者您可以将其视为两个不同的借用,只是绑定到同一个局部变量。这经常发生在循环中(在循环结束时写入变量的新值,并在下一次迭代的顶部最后一次使用它)。
#![allow(unused)] fn main() { let mut data = vec![1, 2, 3]; // This mut allows us to change where the reference points to let mut x = &data[0]; println!("{}", x); // Last use of this borrow data.push(4); x = &data[3]; // We start a new borrow here println!("{}", x); }
过去,Rust 会将借用保持到作用域结束,因此这些示例可能无法使用旧编译器进行编译。此外,在某些情况下,Rust 无法正确缩短借用的活动部分,即使看起来应该编译,也会编译失败。这些问题会随着时间的推移而得到解决。