简介
生命周期对于许多Rust的初学者来说是一个很难理解的概念。它是一个新的概念,以至于大多数程序员从未在其他任何语言中见过它。在本文中,我们剖析生命周期的意义,并提供清晰识别生命周期的方法。
生命周期的目的
在讨论细节之前,让我们首先理解为什么存在生命周期,它们的作用是什么?生命周期可以帮助编译器执行一个简单的规则:任何引用都不应该比它的值活得长。换句话说,生命周期帮助编译器消除悬垂指针错误。编译器通过分析所涉及的变量的生命周期来实现这一点。如果引用的生命周期小于值的生命周期,代码就会编译,否则就不会编译。
“生命周期”这个词的含义
生命周期如此令人困惑的部分原因在于,在Rust的大部分写作中,生命周期这个词被轻率地用于指代三种不同的东西——变量的实际生命周期、生命周期约束条件和生命周期注释。让我们一个一个来看。
变量的生命周期
这是简单的,变量的生命周期是指它存活的时间。这个意思最接近词典中关于事物存在一段时间的含义。例如,在下面的代码中,x的生命周期会一直延伸到外部块的末尾,而y的生命周期会在内部块的末尾结束。
{
let x: Vec<i32> = Vec::new();//---------------------+
{// |
let y = String::from("Why");//---+ | x's lifetime
// | y's 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());// |
}// <-------------------------------------------+
如果没有添加这个约束,println!可以访问x,x是y的引用,而y在上一行中会被销毁。
请注意,约束不会改变实际的生命周期—例如,x的生命周期仍然扩展到外部块的末尾—它们只是编译器用来禁止悬垂引用的工具。在上面的例子中,实际的生命周期不满足约束条件:x的生命周期已经超出了y的生命周期。因此,这段代码无法编译。
生命周期注释
很多时候编译器自动生成生命周期约束,但是随着代码变得更加复杂,编译器会要求程序员手动添加约束,程序员通过生命周期注释来实现这一点。例如,在下面的代码片段中,编译器需要知道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);
}
程序员需要用'a注释s2和返回的引用,从而告诉编译器返回值是从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找到满足约束的具体生命周期引用。
例子
接下来,考虑一个函数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形参注释了参数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 | |
}// <---------------------------+--------------+ |
}// <-------------------------------------------------------------+
通常,当函数有两个或多个引用参数时,返回的引用生命周期不能超过生命周期最短的引用参数。
最后一个例子,许多新的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
}// <----------------------------+
消除规则
当编译器允许程序员省略生命周期注释时,称为生命周期省略。再说一遍,“生命周期省略”这个术语是有误导性的——当生命周期与变量的存在和消失不可分割地联系在一起时,它怎么可能被省略呢?被省略的不是生命周期,而是生命周期注释,以及扩展生命周期约束。在早期版本的Rust编译器中,不允许省略,并且需要每个生命周期注释。但是随着时间的推移,编译器团队观察到生命周期注释的相同模式不断重复。所以制定了消除规则。
在以下情况下,程序员可以省略注释:
当只有一个输入引用时。在这种情况下,将输入的生命周期注释分配给所有输出引用。
例如:
fn some_func(s: &str) -> &str
fn some_func<'a>(s: &'a str) -> &'a str
当有多个输入引用时,但第一个参数是&self或&mut self。在这种情况下,第一个参数的生命周期注释也被分配给所有输出引用。
例如:
fn some_method(&self) -> &str
fn some_method<'a>(&'a self) -> &'a str
总结
变量的生命周期必须满足编译器和程序员对它们施加的某些约束,然后编译器才能确保代码是安全的。如果没有生命周期机制,编译器将无法保证大多数Rust程序的安全性。
本文翻译自:
https://hashrust.com/blog/lifetimes-in-rust/