10.5 生命周期 Pt.1:生命周期的定义与意义、借用检查器与泛型生命周期
10.5.1. 什么是生命周期
Rust 中的每个引用都有自己的生命周期。生命周期的作用是让引用保持有效;换句话说,它就是引用保持有效的作用域。
在大多数情况下,生命周期是隐式的、可推断的。如果引用的生命周期可能以不同方式相互关联,就必须手动标注生命周期。
生命周期可以说是 Rust 与其他语言相比最与众不同的特征,因此非常难学。
10.5.2. 生命周期的存在意义
生命周期存在的主要目的是避免悬空引用。这个概念在 4.4. 引用与借用 中已经讨论过,这里重复一下之前的解释:
在使用指针时,非常容易触发一种叫做悬空指针(Dangling Pointer)的错误。其定义如下:一个指针引用了内存中的某个地址,而这块内存可能已经被释放并重新分配给其他人使用。如果你引用了某些数据,Rust 编译器会保证在引用离开作用域之前,数据不会离开作用域。这就是 Rust 确保悬空引用永远不会出现的方式。
看这个例子:
fn main() { let r; { // 小花括号 let x = 5; r = &x; } println!("{}", r); }- 在这个例子中,先声明了
r但没有初始化。目的是让r存在于小花括号外(见注释位置)的作用域中。当然,Rust 没有Null值,所以在初始化之前不能使用r。 - 在小花括号内,声明了变量
x并赋值为5。下一行把x的引用赋给了r。 - 在那个小花括号作用域结束之后,在外面打印了
r。
这段代码是无效的,因为打印r时,x已经走出作用域并被销毁了。所以r的值——也就是x所引用的内存地址——现在指向的是已经被释放的内存,它所指向的数据也不再是x。这就造成了悬空引用,因此编译器会报错。
输出:
error[E0597]: `x` does not live long enough --> src/main.rs:5:7 | 4 | let x = 5; | - binding `x` declared here 5 | r = &x; | ^^ borrowed value does not live long enough 6 | } | - `x` dropped here while still borrowed 7 | println!("{}", r); | - borrow later used here报错信息说借用的值活得不够长。这是因为内部花括号作用域结束时,x走出了作用域,但r有更大的作用域并可以继续使用。为了保证程序安全,此时任何基于r的操作都无法正确运行。
Rust 通过借用检查器来检查代码是否合法。
10.5.3. 借用检查器
借用检查器通过比较作用域来判断所有借用是否合法。在上面的例子中,借用检查器发现r是对x的引用,但r的存活时间比x更长,因此会报错。
怎么解决这个问题呢?很简单:让x至少活得和r一样长。
fn main() { let x = 5; let r = &x; println!("{}", r); }在这种情况下,x从第 2 行活到第 5 行,r从第 3 行活到第 5 行。因此x的生命周期完全覆盖了r的生命周期,程序不会报错。
10.5.4. 函数中的泛型生命周期
看这个例子:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }string1是String,而string2是字符串切片&str。这两个值被传入longest函数(string1需要先转换成&str),然后打印返回值。longest的逻辑是比较两个输入参数,并返回较长的那个。
输出:
error[E0106]: missing lifetime specifier --> src/main.rs:9:33 | 9 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named 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` help: consider introducing a named lifetime parameter | 9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++错误说缺少生命周期标注,更具体地说是返回类型缺少生命周期参数。正如help文本所说,函数的返回类型包含一个借用值,但函数签名没有说明这个借用值来自x还是来自y。考虑引入一个命名生命周期参数。
再看这个函数:
fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }很明显,这个函数的返回值要么是x,要么是y,但无法预先知道是哪一个。如果只看这个函数本身,两个输入参数x和y的具体生命周期也是未知的。所以,与前面的例子不同,我们无法通过比较作用域来判断返回的引用是否会一直有效。借用检查器也做不到,因为它不知道返回类型的生命周期是与x绑定还是与y绑定。
实际上,即使返回值是固定的,这样写仍然会报错:
fn longest(x: &str, y: &str) -> &str { x }输出:
error[E0106]: missing lifetime specifier --> src/main.rs:9:33 | 9 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named 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` help: consider introducing a named lifetime parameter | 9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++编译器仍然无法判断,因为函数签名没有表达返回类型中的借用值来自哪里。
所以这与函数体内的逻辑无关,完全与函数签名有关。该怎么改呢?可以按照报错信息中的建议来改:
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` help: consider introducing a named lifetime parameter | 9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++既然它让我们添加泛型生命周期参数,我们就添加一个:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }'a表示一个名为a的生命周期。x、y和返回类型都使用生命周期a,这意味着x、y和返回值的生命周期是相同的。
“相同”这个说法并不完全精确,因为main中x和y对应值的实际生命周期其实略有不同。这一点我们会在 10.6 生命周期 Pt.2:生命周期的语法与例子 中讨论。
现在看完整代码:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }输出:
The longest string is abcd