生命周期
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
操作时,我们有一个活动的共享引用 x
,它指向 data
的后代。这将创建一个别名可变引用,这将违反引用的第二条规则。
但是,这根本不是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 无法正确缩短借用的活动部分,即使看起来应该编译,也无法编译。这些问题将随着时间的推移得到解决。