生命周期

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 无法正确缩短借用的活动部分,即使看起来应该编译,也会编译失败。这些问题会随着时间的推移而得到解决。