学习rust生命周期的过程中,作为开发者,很难理解为什么要有这种设计,想要认识到生命周期的作用,就要尝试理解rust为什么要设计生命周期。
函数生命周期的作用
首先要明白生命周期的作用是什么,函数的生命周期参数是为了防止悬垂引用的发生,悬垂引用的意思就是当源数据已经释放了,而对源数据的引用还存在,那这个引用就被成为悬垂引用,C和C++中早就存在这种问题,需要开发者手动保证不会发生悬垂引用,而rust以安全著称,所以编译器要保证不会发生这种情况。
发生悬垂引用的时机
要保证不会发生这个问题,就需要知道什么时机会发生悬垂引用,然后对这些情况做校验。
对于函数而言,函数在执行结束后,就会清理作用域中的数据,而悬垂引用就发生在返回值的位置,如果函数返回一个引用,但是引用的源数据在清理作用域时被销毁了,此时就发生了悬垂引用,在实际编码中,情况还要复杂一些,如果返回的引用指向的源数据来自于函数参数呢?这种情况就不能马上确定是否会发生悬垂引用。下面我们看一下发生问题的两种情况。
一、返回值来源于函数体内创建的变量,必然发生的悬垂引用:
fn foo() -> &String {
let s = String::new();
&s
}
在函数体内创建的变量会在函数调用结束后销毁,所以返回的&s就是悬垂引用。
二、返回值是来源于参数,但不清楚返回哪一个,有可能发生的悬垂引用:
fn longer(s1: &str, s2: &str) -> &str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
对于上面情况,因为代码里存在判断条件,rust在编译阶段是不能确定,最后的返回值是s1还是s2。
当我们不清参数的生命周期时,就有可能发生悬垂引用,考虑下面情况:
let a = String::from("a");
let c;
{
let b = String::from("bb");
// 在编译阶段,rust只能知道调用longer函数
// 但是不清楚返回值是&a还是&b
c = longer(&a, &b);
// 如果返回了&b,将&b绑定给了c
// 但是b在这个作用域结束后被销毁了,
// 所以c变成了悬垂引用
}
println!("{}", c);
防止上面情况的解决方案是,rust在编译阶段能够知道在调用longer后返回值的生命周期,也就是上面c的生命周期,因为rust不清楚longer返回值的生命周期,但是为了防止在运行时上面这种悬垂指针,所以只能在编译期间报错:“不清楚longer函数的返回值生命周期”。
改进编译器的提示
试想,如果rust编译器能够明确提醒开发者哪个变量的生命周期有问题不是更好吗?比如编译器这样报错可以让开发者更明确问题:“c变量的生命周期不够长”
如何实现明确的报错信息
想要明确报错信息,rust除非在编译阶段就能够清楚longer函数返回值c的生命周期长度,如果c绑定的是&b的话,那么c的生命周期就是b的生命周期,也就能够明确报错信息了。
如何确定返回值生命周期
如何让rust在编译阶段能够清楚longer返回值的生命周期?想一下,如果我们能够为函数标出参数生命周期与返回值生命周期的关系,rust是不是就能够清楚返回值的生命周期了,比如上面在定义函数的时候告诉rust:“这个函数的返回值生命周期和两个参数的生命周期是一样长的”,我们可以把这句话用代码的形式标记出来:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
- 像泛型一样,首先在函数名称后边声明出函数中使用到的所有生命周期
- 分别标识每个参数中的生命周期
- 标识出返回值的生命周期
标识完成后,我们一眼就能看出,返回值的生命周期与两个参数的是一样的,也就是说返回值可能是两个参数中的任何一个。有了参数和返回值的关系,rust就可以在编译期间做代码校验:
- 校验一:要求函数体的内容要符合生命周期的声明,返回值&a没有来自于参数中,编译不通过:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
let a = String::new();
&a
}
下面这个例子为每一个参数都分配了自己的生命周期,返回值要求返回生命周期'a,但是函数体内却返回了s2也就是'b,也会编译不通过:
fn longer<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str {
s2
}
- 校验二,要求函数在调用时候的传参符合生命周期的声明,让我们再回到最初的例子:
let a = String::from("a");
let c;
{
let b = String::from("bb");
// 这里会报错&b的生命周期不够长
c = longer(&a, &b);
}
println!("{}", c);
因为我们在函数签名的生命周期的上标注了两个参数和返回值是相同的生命周期,而在上面代码中&b的生命周期明显是要短于&a的,所以报错了,报错信息中很明确的表达了b的生命周期不够长。
最后一句话总结:
生命周期是函数为了检查悬垂引用,对传入的参数要求符合指定的生命周期,只要这么做了就不会产生悬垂引用。