Rust 有一个与非词法生命周期相关的RFC ,它已被批准在该语言中实现很长时间。最近,Rust 对这个特性的支持有了很大的提升,算是完整的了。
我的问题是:非词汇生命周期到底是什么?
通过了解什么是词法生命周期,最容易理解什么是非词法生命周期。在存在非词法生命周期之前的 Rust 版本中,此代码将失败:
fn main() {
let mut scores = vec![1, 2, 3];
let score = &scores[0];
scores.push(4);
}
Rust 编译器看到它scores
被score
变量借用了,所以它不允许进一步的变异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
从未使用过!问题是scores
by的借用score
是词法的——它一直持续到包含它的块的末尾:
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 在实现非词法生命周期之前可以发布并使用很多年。
有趣的是,某些好的模式是由于词汇生命周期而发展起来的。对我来说最好的例子是entry
模式。此代码在非词法生命周期之前失败并使用它进行编译:
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 的借用”。
非词汇生命周期本身从未打算成为“面向用户”的功能。由于我们从他们的缺席中得到的小剪纸,它们在我们的脑海中大部分都变大了。他们的名称主要用于内部开发目的,出于营销目的而更改它从来都不是优先事项。
在 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)]
您甚至可以使用编译器标志选择加入 NLL 的实验版本-Z polonius
。