rust为什么需要生命周期注解
我们知道,rust是通过生命周期等一系列机制来对内存进行管理的。rust中每一个引用都有其生命周期,也就是保持有效的作用域。通常来讲,生命周期都是可推断的,在rust的编译器里有一个借用检查器,它通过比较作用域来保证所有借用都是有效的。举例来说:
fn main() {
let a = 10;
let b = &a;
println!("{}", b);
}
我们可以看到,a的作用域明显比b的作用域要大,而b引用了a,如此一来就可以保证b引用的a在a有效时始终是有效的。反之,若如下:
fn main() {
let b;
{
let a = 10;
b = &a;
}
println!("{}", b);
}
此时,a的作用域比b要小。rust的检查器发现a的生命周期比b要大,但是拥有一个生命周期比它小的对象,编译器就会拒绝编译。简而言之,rust的编译器能帮助我们确定某个引用是否有效(无效的话在编译时就会报错),以此来避免悬垂引用等一系列问题。
然而,现实是,编译器有时候并不能确定某个引用是否有效。我们来看下面这个例子:
fn longest_str(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let str1 = String::from("123");
let str_longer;
{
let str2 = String::from("12345");
str_longer = longest_str(&str1, &str2);
}
println!("{}",str_longer);
}
这段代码是无法编译通过的,报错信息如下:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:37
|
1 | fn longest_str(x: &str, y: &str) -> &str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
error: Could not compile `lifetime_test`.
To learn more, run the command again with --verbose.
表明在函数longest_str里需要生命周期注解,这是为什么呢?我们来分析一下代码。在上面这段代码里,str_longer拥有的引用是否有效呢?答案是不确定。因为如果str_longer的值来自str1则他是有效的,如果来自str2则是无效的。也就是说,这个函数无法确定输入和输出的生命周期的关系。故而像这种函数是不合法的,必须进行生命周期注解。
需要注意的是,生命周期注解并不改变任何引用的生命周期的长短。也就是说,单个生命周期其实是没什么意义的,生命周期最大的作用就是告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。加入有一个有一个生命周期为'a
的参数x,一个生命周期也为'a
的参数y,那么就说明x与y的生命周期是一样长的。这么一来就可以解决刚才longest_str
函数的问题了。我们给函数标上生命周期注解:
fn longest_str<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let str1 = String::from("123");
let str_longer;
{
let str2 = String::from("12345");
str_longer = longest_str(&str1, &str2);
}
println!("{}",str_longer);
}
虽然仍然无法编译通过,但是错误信息变成了:
error[E0597]: `str2` does not live long enough
--> src/main.rs:14:41
|
14 | str_longer = longest_str(&str1, &str2);
| ^^^^^ borrowed value does not live long enough
15 | }
| - `str2` dropped here while still borrowed
16 |
17 | println!("{}",str_longer);
| ---------- borrow later used here
即错误信息变成了编译器检查出引用生命周期无效了,而不是longest_str
这个函数的错误了,即str2和str1生命周期不一致。我们只需要修改main
函数里参数的生命周期,使str1与str2生命周期保持一致即可。
fn longest_str<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let str1 = String::from("12345");
let str_longer;
let str2 = String::from("123");
str_longer = longest_str(&str1, &str2);
println!("{}",str_longer);
}
那么问题来了,在上面这段代码里,函数longest_str
里,x,y和函数返回值的生命周期都是一致的,那我们可不可以将它们标注成不同的呢?就像
fn longest_str<'a, 'b, 'c>(x: &'a str, y: &'b str) -> &'c str {
if x.len() > y.len() {
x
} else {
y
}
}
很遗憾,这样是不行的。我们之前提到,生命周期注解并不改变任何引用的生命周期的长短,增加泛型生命周期参数来定义引用间的关系是为了以便借用检查器可以进行分析。如果我们给这三个值标注不同的生命周期,那么和没有任何标注本质上并没有什么区别。那么我们可以给x,y分别标注不同的周期,但返回值和这两个值其中一个周期相同吗?答案还是不行,因为有if语句的存在,我们没法确定返回值和那一个参数的生命周期一致。
最后,总结一下,当且仅当输出值的生命周期为所有输入值的生命周期交集的子集时,生命周期合法。这个应该很好理解,也很好的解释了我们举的例子中生命周期注解的用法。rust也正是通过这种方式解决了"共享"这一问题,从某种程度上保证了内存的安全。