乐趣区

关于rust:你所不了解的-Rust-生命周期

Rust – 生命周期

原文:https://hashrust.com/blog/lifetimes-in-rust/

译者:韩玄亮(一个酷爱开源,喜爱 Rust 的 go 开发者)

介绍

对于很多 Rust 的初学者来说,生命周期 (lifetime) 是一个很难把握的概念。我也为此挣扎了一段时间,才开始明确它们对 Rust 编译器执行其职责 (move/borrow) 是如许重要。lifetime 自身并不难。只是它们是看似新鲜的构造,以至大多数程序员都没在其余语言中见过它。更蹩脚的是,人们过多地应用 “lifetime” 这个词来议论很多密切相关的问题。在本文中,我将把这些想法离开,这样做是为了给你提供清晰地思考 lifetime 的工具。

目标

在探讨细节之前,让咱们先理解一下为什么须要生命周期。它们的目标是什么? 生命周期能够帮忙编译器执行一个简略的规定: 任何援用自身都不能比它援用的对象存活地更久。换句话说,生命周期帮忙编译器打消悬空指针谬误(注:也就是说在援用的状况下才会呈现生命周期标注)。

在上面的例子中你将看到,编译器通过剖析相干变量的生命周期来实现这一点。如果援用的 lifetime 小于被援用的 lifetime,编译胜利,否则编译失败。

“lifetime”

生命周期如此令人困惑的局部起因是在 Rust 的大部分文章中,生命周期这个词被松散地用于指代三种不同的货色:

  • 变量的理论生命周期
  • 生命周期束缚
  • 生命周期正文

上面让咱们一个一个地来谈谈。

variables 生命周期

代码中变量之间的交互模式对它们的 lifetime 产生了一些限度。例如,在上面的代码中:x = &y; 这一行增加了一个束缚,即:x 的 lifetime 应该蕴含在 y 的 lifetime 内 (x ≤ y):

//error:`y` does not live long enough
{
let x: &Vec<i32>;
    {let y =Vec::new();//----+
//                               | y's lifetime
//                               |
        x = &y;//----------------|--------------+
//                               |              |
    }// <------------------------+              | x's lifetime
    println!("x's length is {}", x.len());//    |
}// <-------------------------------------------+

如果没有增加这个束缚,x 就会在 println! 代码块中拜访有效的内存。因为 x 是对 y 的援用,它将在前一行被销毁。

须要留神的是:束缚不会扭转理论的生存期 —— 例如,x 的 lifetime 实际上依然会扩大到内部块的开端 —— lifetime 只是编译器用来禁止悬空援用的工具。在下面的例子中,理论的生存期不满足束缚:x 的 lifetime 曾经超出了 y 的 lifetime。因而,这段代码无奈编译。

生命周期正文

如上一节所示,很多时候编译器会 (主动) 生成所有的 lifetime 束缚。然而随着代码变得越来越简单,编译器会要求开发者手动增加束缚。程序员通过生命周期正文来实现这一点。例如,在上面的代码中,编译器须要晓得 print_ret() 返回的援用是借用了 s1 还是 s2,所以编译器要求程序员显式地增加这个束缚:

// error:missing lifetime specifier
// this function's return type contains a borrowed value,
// but the signature does not say whether it is borrowed from `s1` or `s2`
fn print_ret(s1: &str,s2: &str) -> &str{println!("s1 is {}", s1);
    s2
}
fn main() {let some_str:String= "Some string".to_string();
        let other_str:String= "Other string".to_string();
        let s1 = print_ret(&some_str, &other_str);
}

📒:
如果您想晓得为什么编译器不能看到输入援用是从 s2 中借来的,能够看看这个答复:here。要查看编译器何时能够省略生命期正文,请参阅上面的 lifetime 省略局部。

而后,开发者用 'a' 标记 s2 和返回的援用,用来通知编译器,返回值是从 s2 中借来的。

fn print_ret<'a>(s1: &str,s2: &'astr) -> &'astr{println!("s1 is {}", s1);
    s2
}
fn main() {let some_str:String= "Some string".to_string();
        let other_str:String= "Other string".to_string();
        let s1 = print_ret(&some_str, &other_str);
}

不过我想强调的是,仅仅因为 'a 标记在参数 s2 和返回的援用上,并不意味着 s2 和返回的援用都有完全相同的 lifetime。相同,这应该被了解为:带有 ‘a 标记的返回援用是从具备雷同标记的参数中借用来的。

因为 s2 进一步借用了 other_str,lifetime 束缚是返回的援用不能超过 other_str 的 lifetime。这里满足束缚,编译胜利:

fn print_ret<'a>(s1: &str, s2: &'a str) -> &'a str {println!("s1 is {}", s1);
    s2
}
fn main() {let some_str: String = "Some string".to_string();
    let other_str: String = "Other string".to_string();//-------------+
    let ret = print_ret(&some_str, &other_str);//---+                 | other_str's lifetime
    //                                              | ret's lifetime  |
}// <-----------------------------------------------+-----------------+

在展现更多示例之前,简要介绍一下 lifetime 正文语法。

要创立 lifetime 正文,必须首先申明 lifetime 参数。例如,<'a> 是一个生命周期申明。lifetime 参数是一种通用的参数,您能够将 <'a> 读为 "for some Lifetime'a…"。一旦申明了 lifetime 参数,就能够在其余援用中应用它来创立 lifetime 束缚。

记住,通过用 'a 标记援用,程序员只是在结构一些束缚; 而后,编译器的工作就是为 'a 找到满足其束缚的具体生存期。

示例

接下来,思考一个求出两个值的最小值的函数:

fn min<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
    if x < y {x} else {y}
}
fn main() {
    let p = 42;
    {
        let q = 10;
        let r = min(&p, &q);
        println!("Min is {}", r);
    }
}

在这里,'a lifetime 正文标记了参数 x、y 和返回值。这意味着返回值能够从 x 或 y 中借用。而又因为 x 和 y 别离进一步从 p 和 q 中借用,所以返回的援用的生命周期应该蕴含在 p 和 q 的生命周期中。这段代码也能够编译,满足了束缚:

fn min<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
    if x < y {x} else {y}
}
fn main() {
    let p = 42;//-------------------------------------------------+
    {//                                                           |
        let q = 10;//------------------------------+              | p's lifetime
        let r = min(&p, &q);//------+              | q's lifetime |
        println!("Min is {}", r);// | r's lifetime |              |
    }// <---------------------------+--------------+              |
}// <-------------------------------------------------------------+

通常,当同一 lifetime 参数标记一个函数的两个或多个形参时,返回的援用不能超过形参最小的 lifetime。

最初一个例子,这是许多 C++ 老手开发者犯的一个谬误,即返回一个指向局部变量的指针。而在 Rust 中不容许有相似的行为:

//Error:cannot return reference to local variable `i`
fn get_int_ref<'a>() -> &'a i32 {
    let i: i32 = 42;
    &i
}
fn main() {let j = get_int_ref();
}

因为 get_int_ref() 没有参数,因而编译器晓得返回的援用得从局部变量中借用,而这是不容许的。编译器正确地防止了 bug,因为当返回的援用试图拜访它指向的内存时,局部变量将被清理掉。

fn get_int_ref<'a>() -> &'a i32 {
    let i: i32 = 42;//-------+
    &i//                     | i's lifetime
}// <------------------------+
fn main() {let j = get_int_ref();//-----+
//                               | j's lifetime
}// <----------------------------+

省略的场景

当编译器容许开发者省略 lifetime 正文时,称为 lifetime 省略。再说一遍,“生命周期省略”一词也具备误导性 —— lifetime 与变量的产生和销毁有着密不可分的关系,又怎么可能省略 lifetime 呢?

所以被省略的不是 lifetime,而是 lifetime 正文和扩大的 lifetime 束缚。在 Rust 编译器的晚期版本中,不容许省略,并且须要对每个生命周期进行标注。但随着工夫的推移,编译器团队察看到生命周期正文的雷同模式被反复,因而批改编译器规定,从而推断它们。

在以下状况下,程序员能够省略标记:

  1. 当只有一个输出参数援用时:在这种状况下,输出参数的生命周期正文会被赋值给所有输入参数援用。例如: fn some_func(s: &str) -> &str 被推断为 fn some_func<'a>(s: &'a str) -> &'a str
  2. 当有多个传入参数援用时,但第一个参数是:&self/&mut self:在这种状况下,输出参数的生命周期正文也被赋值给所有输入援用。例如: fn some_method(&self) -> &str 等价于 fn some_method<'a>(&'a self) -> &'a str

lifetime 标注省略缩小了代码中的凌乱,将来编译器可能会推断出更多模式的 lifetime 束缚。

总结

许多 Rust 老手发现 lifetime 这个话题很难了解。但 lifetime 自身并不是问题所在,而是这个概念在 Rust 很多文章中所出现的形式。在本文中,我试图梳理出 “lifetime” 这个词的适度应用背地暗藏的含意。

变量的生命周期必须满足编译器和开发者对它们施加的某些束缚,而后编译器能力确保代码是正当的。如果没有 lifetime 这种机制,编译器将无奈保障大多数 Rust 程序的安全性。

退出移动版