Rust中的Non-lexical lifetimes:它是什么以及如何简化代码

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

什么是non-lexical lifetimes?认识Rust的主要改进

\n\n

每个写过Rust至少几周的人,都遇到过这种情况:代码完全合乎逻辑且安全,但编译器(更准确地说,是其严格的borrow checker)拒绝编译。你看着错误提示想:“但我已经不再使用这个引用了!为什么它还在报错?”

\n\n

在Rust 1.31版本(2018 edition)之前,答案很简单:“因为变量的生命周期(lifetime)是由作用域——即词法作用域——决定的。”这就是lexical lifetimes。从Rust 2018开始,编译器变得更聪明了。它引入了non-lexical lifetimes(NLL)——非词法生命周期。

\n\n

在本文中,我们将探讨这是什么,为什么它的出现对Rust开发者来说是一场革命,以及它如何让代码更简洁、更直观。我们不会深入理论类型的细节,而是专注于实际示例。

\n\n

Lexical lifetimes的问题:为什么borrow checker过于严格

\n\n

让我们回顾一下Rust中借用(borrowing)的工作原理。规则如下:

\n
    \n
  • 在任何时刻,要么有一个可变引用(&mut T),要么有任意数量的不可变引用(&T)。
  • \n
  • 引用必须始终有效(不能引用已释放的数据)。
  • \n
\n\n

在NLL之前,编译器根据引用的词法作用域来确定其生命周期。也就是说,从{到},即变量声明的地方。如果引用是在块内创建的,它会“存活”到该块的末尾,即使你不再使用它。

\n\n

看看这个经典示例,它在旧版Rust中无法编译:

\n\n
fn main() {\n    let mut data = vec![1, 2, 3];\n\n    let x = &data[0];  // 不可变引用\n\n    data.push(4);       // 尝试可变借用\n\n    println!("{}", x);  // 使用不可变引用\n}
\n\n

从人的角度来看:我们创建了引用x,然后修改了向量,最后使用了x。但是,当调用push时,向量data可能会重新分配内存,导致引用x失效。编译器是对的——这很危险。

\n\n

但是,如果我们在修改之前使用引用呢?这才是真正的痛点:

\n\n
fn main() {\n    let mut name = String::from("Alice");\n\n    let first_char = &name[0..1]; // 不可变引用\n    println!("First char: {}", first_char); // 使用它\n\n    name.push_str(" Johnson"); // 修改字符串\n    println!("Full name: {}", name);\n}
\n\n

这段代码是安全的。我们使用了引用first_char,打印了它,然后才修改了字符串。但在Rust 2018之前,编译器会报错!为什么?因为引用first_char的词法生命周期持续到函数main的末尾,即到右花括号为止。而在这个区间内的可变借用(push_str)是被禁止的。

\n\n

开发者不得不使用变通方法:引入额外的块{}来手动限制引用的生命周期:

\n\n
fn main() {\n    let mut name = String::from("Alice");\n\n    {\n        let first_char = &name[0..1];\n        println!("First char: {}", first_char);\n    } // 引用 first_char 在此处失效\n\n    name.push_str(" Johnson");\n    println!("Full name: {}", name);\n}
\n\n

这虽然可行,但不方便,用多余的括号污染了代码,并增加了阅读难度。尤其是在涉及多个借用的复杂函数中。

\n\n

Non-lexical lifetimes(NLL):它是如何工作的

\n\n

NLL的理念简单而巧妙:引用的生命周期不由作用域决定,而是由它实际被使用的位置决定。

博客

书籍推荐