【问题标题】:What are non-lexical lifetimes?什么是非词汇生命周期?
【发布时间】:2020-01-06 10:15:18
【问题描述】:

Rust 有一个RFC 与非词汇生命周期相关,has been approved 将在该语言中长期实现。 Recently,Rust 对这个特性的支持提升了很多,算是完善了。

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

【问题讨论】:

    标签: rust terminology lifetime lifetime-scoping


    【解决方案1】:

    通过了解lexical 生命周期是什么,最容易理解非词汇生命周期是什么。在存在非词法生命周期之前的 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从未使用过!问题是score借用scoreslexical——它一直持续到包含它的块的末尾:

    fn main() {
        let mut scores = vec![1, 2, 3]; //
        let score = &scores[0];         //
        scores.push(4);                 //
                                        // <-- score stops borrowing here
    }
    

    非词法生命周期通过增强编译器以了解此详细程度来解决此问题。编译器现在可以更准确地判断何时需要借用,并且此代码将编译。

    非词法生命周期的美妙之处在于,一旦启用,没有人会想到它们。它将简单地成为“Rust 所做的事情”,并且事情会(希望)正常工作。

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

    Rust 只允许编译已知安全的程序。但是,it is impossible 完全允许 安全程序并拒绝不安全程序。为此,Rust 在保守方面犯了错误:一些安全程序被拒绝。词汇生命周期就是一个例子。

    词法生命周期在编译器中更容易实现,因为块的知识是“微不足道的”,而数据流的知识则不那么重要。编译器必须是rewritten to introduce and make use of a "mid-level intermediate representation" (MIR)。然后必须重写借用检查器(又名“borrowck”)以使用 MIR 而不是抽象语法树(AST)。然后必须细化借用检查器的规则以使其更细。

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

    有趣的是,某些 good 模式是由于词汇生命周期而开发的。对我来说最好的例子是the entry pattern。此代码在非词法生命周期之前失败并使用它进行编译:

    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;
    }
    

    “非词汇生命周期”这个名字对我来说听起来不合适

    值的生命周期是值停留在特定内存地址的时间跨度(请参阅Why can't I store a value and a reference to that value in the same struct? 以获得更长的解释)。被称为非词法生命周期的特性不会更改任何值的生命周期,因此它不能使生命周期成为非词法的。它只会使对这些值的借用的跟踪和检查更加精确。

    该功能的更准确名称可能是“非词法借用”。一些编译器开发人员参考了底层的“基于 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)]
    

    您甚至可以使用编译器标志 -Z polonius 选择加入 NLL 的实验版本。

    非词汇生命周期解决的实际问题示例

    【讨论】:

    • 我认为值得强调的是,也许与直觉相反,非词汇生命周期不是关于变量的生命周期,而是关于借用的生命周期。或者,换句话说,非词汇生命周期是关于将变量的生命周期与借用的生命周期去相关......除非我错了? (但我认为执行析构函数时 NLL 不会改变)
    • 有趣的是,某些好的模式是由于词汇生命周期而开发的”——那么,我想,NLL 的存在可能会使未来的好模式变得更加困难识别?
    • @eggyal 这当然是可能的。在一组约束内进行设计(即使是任意的!)可以带来新的、有趣的设计。如果没有这些限制,我们可能会依赖我们现有的知识和模式,而永远不会学习或探索以找到新的东西。话虽这么说,大概有人会想“哦,哈希计算了两次,我可以解决这个问题”,然后创建 API,但用户一开始可能更难找到 API。我希望像 clippy 这样的工具可以帮助这些人。
    • 也许更好的命名改进是,sub-lexical生命周期,考虑到它特别缩短了绑定的生命周期估计。此外,如前所述,地址粘性与生命周期没有任何关系,因为附加到向量 (push) 可以强制重新分配并因此更改其地址而不会丢失其绑定的引用。对于这个新手来说,生命周期系统似乎都是关于绑定的:所有者、借款者和观察者(也称为共享)。想想看,Rust 中的观察者模式可能非常简单。
    猜你喜欢
    • 2022-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多