bug生命周期_【译】Rust中的生命周期

50f83f9283202dda97ae291e4d657dba.png

原文链接: https:// hashrust.com/blog/lifet imes-in-rust/
原文标题: Lifetimes in Rust
公众号: Rust 碎碎念

简介

对于很多 Rust 初学者而言,生命周期(lifetime)是一个很难掌握的概念。在我意识到生命周期对 Rust 编译器履行职责有多重要之前,也曾与其抗争过一段时间。生命周期本质上并不难。只是因为它是一种新的构思,以至于大多数程序员从未在任何其他语言中见过它们。让事情变得更糟的是,人们过度使用生命周期(lifetime) 这个词来讨论许多密切相关的思想。在本文中,我会将这些思想进行区分,并以此使你能对生命周期有清晰的思考。

生命周期的目标(Purpose of lifetimes)

在讨论具体内容之前,让我们先来理解生命周期为什么会存在。它们被用于什么目标?生命周期帮助编译器执行一个简单的规则:引用不应该活得比所指对象长(no reference should outlive its referent)。换句话说,生命周期帮助编译器压制悬垂指针 bug。正如你将会在下面的例子中所见,编译器通过分析相关变量的生命周期来实现这一目标。如果一个引用的生命周期小于其所指对象的生命周期,则代码可以编译,否则就无法编译。

生命周期一词的意义(Meaning of the word lifetime)

生命周期之所以如此令人困惑,其部分原因在于 Rust 的很多写作中,生命周期这个词被宽泛地用来指代三种不同的东西——变量真实的生命周期、生命周期约束和生命周期标注。让我们来一个一个的看。

变量的生命周期(Lifetimes of variables)

这很直观,变量的生命周期是指它活着的时间。这个意思最接近于字典里的定义,即事物存在或有效的持续时间(the duration of a thing's existence or usefulness) 。例如,在下面的代码中,x的生命周期延续到外部块的结尾,而y的生命周期在内部块的结尾就结束了。

{
    let x: Vec<i32> = Vec::new();//---------------------+
    {//                                                 |
        let y = String::from("Why");//---+              | x's lifetime
        //                               | y's lifetime |
    }// <--------------------------------+              |
}// <---------------------------------------------------+

生命周期约束(Lifetime constraints)

变量在代码中的交互方式对它们的生命周期有一定的约束。例如,在下面的代码中,x=&y;这一行添加了一个约束,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!这行代码中访问无效的内存,因为xy的一个引用,而y会在前面的一行被销毁。
记住,约束没有改变真实的生命周期——例如,x的生命周期,仍然延展到外部块的结尾,它们只是编译器用来禁止悬垂引用的工具。并且在上面的例子中,真实的生命周期没有满足约束:x的生命周期超出y的生命周期。因此,这段代码会编译失败。

生命周期标注(Lifetime annotations)

正如在上一节所见,很多次编译器都会生成所有的生命周期约束。但是随着代码愈加复杂,编译器会让程序员手动添加约束。程序员通过生命周期标注来完成这件事。例如,在下面的代码片段里,编译器需要知道print_net函数返回的引用是借用了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,请去阅读 这个 StackOverflow 上的回答[1]。想知道什么时候编译器让你省略标注,看下面的 生命周期省略 部分。

然后程序员就对s2和返回的引用使用'a进行标注,从而告诉编译器返回值借用自s2

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 s1 = print_ret(&some_str, &other_str);
}

我想强调的是,生命周期标注'a在参数s2和返回的引用上都出现了,不要把这解释为s2和返回的引用有完全相同的生命周期,而应该这样解释:使用'a标注的返回的引用借用自具有相同标注的参数。
因为s2借用自other_str,这里的生命周期约束是,返回的引用必须不能活得比other_str长。代码可以编译是因为生命周期约束得到了满足:

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  |
}// <-----------------------------------------------+-----------------+

在向你展示更多的例子之前,让我简要介绍一下生命周期标注语法。要创建一个生命周期标注,必须先声明一个生命周期参数。例如,<'a>是一个生命周期声明。生命周期参数是一种泛型参数,你可以把<'a>读作"对于某生命周期'a..."。一旦一个生命周期参数被声明,它就可以在引用中使用以创建一个生命周期约束。
记住,通过使用'a标注引用,程序员只是明确表述某些约束;而为满足约束的'a找到一个具体的生命周期则是编译器的工作。

更多示例(More examples)

接下来,考虑一个函数min,其作用是找到两个值中最小的那个:

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生命周期参数标注了参数xy以及返回值。它表示,返回值可以借用自xy。因为xy分别借用自pq,所以返回的引用的生命周期应该同时封闭于pq之内。这段代码可以编译通过是因为约束得到了满足:

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 |              |
    }// <---------------------------+--------------+              |
}// <-------------------------------------------------------------+

一般而言,当相同的生命周期参数标注了一个函数中两个及以上的参数,返回的引用必须不能活得比参数生命周期中最小的那个长。
最后一个例子。许多 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中没有参数,所以编译器知道,返回的引用必定是借用自一个局部变量,而这是不被允许的。编译器正确地避过了灾难,因为当返回的引用尝试访问局部变量时,该变量已经被清除:

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 elision)

当编译器让程序员省略生命周期标注时,被称为生命周期省略(lifetime elision)。再一次地,生命周期省略被误解——生命周期与变量的产生和销毁有着密不可分的联系,怎么能被省略呢?被省略的不是生命周期,而是生命周期标注以及对应的生命周期约束。在 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

生命周期省略减少了代码中的杂乱性,并且有可能在将来,编译器能够对更多的模式推导出生命周期约束。

总结(Conclusion)

许多 Rust 新手发现生命周期这个主题难以理解。但是生命周期本身并不应该被指责,应该指责的是这个概念在很多 Rust 的写作中是如何被呈现的。在本文中,我已经试图讲明被过度使用的生命周期一词的含义。
在编译器在确保代码合理之前,变量的生命周期必须满足被编译器和程序员施加的特定约束。没有生命周期这个设施,编译器将无法保证大多数 Rust 程序的安全性。

参考资料

[1]

这个StackOverflow上的回答: https://stackoverflow.com/a/31612025

禁止转载,谢谢配合。欢迎关注个人公众号: Rust碎碎念

7642f549b34cbdac39e749b41ab28d10.gif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值