非词法生命周期是什么?

149

Rust有一个与非词汇生命周期相关的RFC,该已被批准长期实施于该语言中。最近, Rust对此功能的支持得到了很大改善,并被认为是完整的。

我的问题是:什么是非词汇生命周期?

1个回答

235

了解什么是非词汇生命周期最简单的方法是了解什么是词汇 生命周期。在 Rust 的非词汇生命周期版本之前,以下代码将失败:

fn main() {
    let mut scores = vec![1, 2, 3];
    let score = &scores[0];
    scores.push(4);
}
Rust编译器发现scoresscore变量借用,因此禁止对scores进行进一步修改。
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let score = &scores[0];
  |                  ------ immutable borrow occurs here
4 |     scores.push(4);
  |     ^^^^^^ mutable borrow occurs here
5 | }
  | - immutable borrow ends here
然而,人类可以轻易地看出此示例过于保守:score从未被使用!问题在于scoresscore的借用是词法作用域 - 它一直持续到包含它的块结束。
fn main() {
    let mut scores = vec![1, 2, 3]; //
    let score = &scores[0];         //
    scores.push(4);                 //
                                    // <-- score stops borrowing here
}

非词法生命周期通过增强编译器对此级别的细节的理解来解决了这个问题。编译器现在可以更准确地判断何时需要借用,从而使得代码可以编译。

非词法生命周期的一个美妙之处是,一旦启用,没有人会再去考虑它们。它将简单地成为“Rust所做的事情”,并且一切(希望如此)都会正常工作。

为什么允许词法生命周期?

Rust旨在只允许已知安全的程序进行编译。然而,精确地只允许 安全程序而拒绝不安全程序是不可能的。因此,Rust在保守方面出错:有些安全程序被拒绝。词法生命周期就是其中之一。

词法生命周期在编译器中实现起来要容易得多,因为块的知识是“平凡的”,而数据流的知识则不太容易。编译器需要被重写以引入和利用“中级中间表示”(MIR)。然后,借用检查器(也称为“borrowck”)必须被重写以使用MIR而不是抽象语法树(AST)。接着,借用检查器的规则必须被细化。

词法生命周期不总是妨碍程序员,当它们确实妨碍时有许多解决方法,即使它们很烦人。在许多情况下,这涉及添加额外的花括号或布尔值。这使得Rust 1.0可以发布并且在非词法生命周期实现之前有多年的实用性。

有趣的是,由于词法生命周期,出现了某些的模式。对我来说,最好的例子就是条目模式。这段代码在非词法生命周期之前会失败,在其实现后编译通过:

fn example(mut map: HashMap<i32, i32>, key: i32) {
    match map.get_mut(&key) {
        Some(value) => *value += 1,
        None => {
            map.insert(key, 1);
        }
    }
}

然而,这段代码效率低下,因为它对键进行了两次哈希计算。由于词法生命期创建的解决方案更短、更高效:

fn example(mut map: HashMap<i32, i32>, key: i32) {
    *map.entry(key).or_insert(0) += 1;
}

“非词法生命周期”这个名字听起来不对劲

一个值的生命周期是指该值在特定内存地址中保留的时间段(有关更长的解释,请参见为什么我不能在同一个结构体中存储一个值和对该值的引用?)。所谓非词法生命周期并没有改变任何值的生命周期,因此它也不能使生命周期成为非词法的。它只是使跟踪和检查那些值的借用更加精确。

这个特性的更准确的名称可能是“非词法借用”。一些编译器开发人员称之为基于MIR的borrowck。

非词法生命周期从来没有被打算成为一个“面向用户”的特性。它们在我们的心目中大多是因为缺少它们而受到伤害。它们的名字主要是为了内部开发目的而设计的,并且出于营销目的而更改它们的名称从来不是一个优先事项。

那么,我该如何使用它呢?

在Rust 1.31(发布于2018-12-06)中,您需要在Cargo.toml中选择Rust 2018版:

[package]
name = "foo"
version = "0.0.1"
authors = ["An Devloper <an.devloper@example.com>"]
edition = "2018"

从Rust 1.36开始,Rust 2015版也支持非词法生命周期。目前的非词法生命周期实现处于“迁移模式”。如果NLL借用检查器通过,则继续编译。如果未通过,则调用先前的借用检查器。如果旧的借用检查器允许代码运行,则会打印警告消息,提醒您的代码可能会在未来版本的Rust中发生变化,应进行更新。

在Rust的夜版中,您可以通过功能标志选择强制性破坏:

#![feature(nll)]

您甚至可以使用编译器标志-Z Polonius选择加入非词法生命周期(NLL)实验版本。

非词法生命周期解决的真实问题示例


15
我认为值得强调的是,也许与直觉相反,非词法生命周期并不涉及变量的生命周期,而是涉及借用的生命周期。换句话说,非词法生命周期是关于将变量的寿命与借用的寿命解耦...除非我错了?(但我认为当执行析构函数时,NLL不会发生改变) - Matthieu M.
4
有趣的是,由于词汇生命周期的存在,出现了一些好的编程模式。那么我想,存在NLL可能会增加未来识别好的模式的难度? - eggyal
4
@eggyal 这当然是一种可能性。在一组约束条件下设计(即使这些条件是任意的!)可以引导我们产生新的、有趣的设计。如果没有这些限制,我们可能会回到我们已有的知识和模式中,并且永远不会学习或探索寻找新的东西。话虽如此,想必会有人会想:“哦,哈希被计算了两次,我可以修复它”,从而创建API,但用户可能更难找到这个API。希望像 clippy 这样的工具能帮助那些人们。 - Shepmaster
2
也许更好的命名细化应该是“子词汇生命周期”,因为它特别缩短了绑定的寿命估计。此外,如上所述,地址粘性与生命周期无关,因为向向量(push)添加元素可能会强制重新分配,从而更改其地址,但不会丢失其绑定。对于这位新手来说,生命周期系统似乎都围绕着绑定:所有者、借用者和观察者(也被称为共享)。想想看,在Rust中的观察者模式可能非常简单。 - George
https://dev59.com/y1oT5IYBdhLWcg3w6i23 中NLL解决的第一个问题,在 Rust 2021 1.67 中仍无法编译通过。这将通过 polonius borrowchk 得到修复。 - zombiesauce
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接