Rust 生命周期机制是与所有权机制同等重要的资源管理机制。生命周期,简而言之就是引用的有效作用域,之所以引入这个概念主要是应对复杂类型系统中资源管理的问题。引用是对待复杂类型时必不可少的机制,毕竟复杂类型的数据不能被处理器轻易地复制和计算,但引用往往导致极其复杂的资源管理问题。
生命周期的主要目标:避免悬垂引用(dangling reference)
fn test01() {
{
let r;
{
let x = 3;
r = &x;
}
println!("{}", r);
}
}
上面这段代码会在 r = &x;
处报错,因为当打印 r
的值的时候,x
已经离开了他的作用域,这时 r
指向的 x
的内存已经被释放,因此会报错。
Rust 实际上是通过借用检查器来检查一些变量的生命周期。
Rust 编译器的借用检查器(borrow checker),用来比较作用域来判断所有的借用是否合法
在上例中,借用检查器检测到 r
的生命周期大于 x
,即被引用者的生命周期小于引用者的生命周期,因此编译会报错。
fn test02() {
let string1 = String::from("Congratulations");
let string2 = "fantastic";
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
}
}
编译报错:
➜ ~/Code/rust/life_cycle git:(master) ✗ cargo run
Compiling life_cycle v0.1.0 (/home/cherry/Code/rust/life_cycle)
error[E0106]: missing lifetime specifier
--> src/main.rs:24:33
|
24 | 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
|
24 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
我们发现编译器会提示缺少一个命名的生命周期参数,这个函数返回一个借用的值,但是没有声明这个借用的值是来自 x
还是来自 y
。值得说明的是,这个返回值的借用跟函数体的逻辑没有关系,要从函数签名就要看出返回值借用的值来自哪一个参数。
根据编译器提示,我们声明一个泛型生命周期 'a
,代码修改如下:
fn longest<'a> (x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
生命周期参数名语法如下:
'
开头'a
表示生命周期标注的位置:
&
后面标注生命周期标注的例子:
&i32 // 一个引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
值得注意的是,单个生命周期标注本身没有意义,我们再看上面的 longest
函数:
fn longest<'a> (x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
泛型的生命周期参数声明在函数名和参数列表之间的 <>
中。
我们仔细分析这个函数:
longest
函数的两个参数都声明了生命周期,就要求这两个引用必须和泛型的生命周期存活相同的时间,而且函数所返回的字符串切片的存活时长也不能小于 'a
这个生命周期。为引用指明生命周期,是要确保当引用失去了所有权后而被移出内存。当在函数参数中指明生命周期时,我们并没有改变参数和返回值的生命周期,只是向调用检查器指出了一些可用于检查非法调用的约束。而 longest
函数本身并不需要知道参数 x
和 y
具体的存活时长,只需要某个可以代替 'a
的作用域,同时满足函数的签名约束。实际上,若函数引用其外部的代码或者被外部代码引用,只靠 rust 本身确定参数和返回值的生命周期时不可能的,这样的话,函数所使用的生命周期在每次调用中都会发生变化,正因为如此,我们才需要手动对生命周期进行标注。
当我们将两个引用传入函数时,x
和 y
作用域重叠的部分将用来代替 'a
这个生命周期的作用域,换句话说,这个泛型生命周期得到的具体的生命周期就是 x
和 y
两者生命周期较短的那个,因为返回值也标注了相同的生命周期,因此返回值的引用在两者比较短的生命周期内都是有效的。
那么生命周期标注是如何对 longest
函数进行限制的?我么修改一下代码:
fn test02() {
let string1 = String::from("Congratulations");
{
let string2 = "fantastic";
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 下面三行代码放到一个单独的作用域里,string2 是一个字符串字面值(字符串切片),他的生命周期相当于是一个静态的生命周期,在整个程序运行期间都存活,而 result 引用也会在 Line 7 大括号结束之前保持有效,因此代码不会报错。
【注】:&str
是直接在可执行文件中加载的,即这块内存直接放到可执行文件里面的,所以整个程序运行期间,这块内存比较特殊,不会由于所有权而消失,所以指这块内存的引用,一定会一直指向一个合法内存,所以其引用的生命周期是 'static
,也就是全局静态,也不可能出现什么悬垂引用。
再改一下代码,将 result 声明放到外面,然后将 print 也放到外面,将 string2 改成 String 类型:
fn test02() {
let string1 = String::from("Congratulations");
let result;
{
let string2 = String::from("fantastic");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
我们发现 Line 6 报错了。string1 的生命周期为 Line 2~9,string2 的生命周期为 Line 5~7,所以 'a
所表示的生命周期为 Line 5~7,而 result 的生命周期为 Line 3~9,不在 'a
的范围内,因此编译报错,我们来看一下编译具体的错误:
➜ ~/Code/rust/life_cycle git:(master) ✗ cargo run
Compiling life_cycle v0.1.0 (/home/cherry/Code/rust/life_cycle)
error[E0597]: `string2` does not live long enough
--> src/main.rs:30:44
|
30 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
31 | }
| - `string2` dropped here while still borrowed
32 | println!("The longest string is {}", result);
| ------ borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `life_cycle` due to previous error
这个报错的含义是,为了让 result 这个变量在打印时是有效的,那么 string2 必须在外部作用域结束之前一直保持有效,因为在函数声明中参数和返回值都使用了相同的生命周期。
在上例中,尽管 string1 的长度大于 string2 的长度,函数返回的是 string1 的引用,但是编译器并不知道这一点,编译器只知道 longest
函数返回引用的生命周期是 x
和 y
生命周期比较短的那个。
指定生命周期参数的方式依赖于函数所做的事情,在上面的例子中,若 longest
函数改为:
fn longest(x: &str, y: &str) -> &str {
x
}
这个时候,函数只返回变量 x
,而与 y
无关,因此无需为 y
指定生命周期。
如果返回的引用没有指向任何参数,那么他只能引用函数内创建的值
这就是悬垂引用,该值在函数结束时就走出了作用域,见下面的例子
fn test02() {
let string1 = String::from("Congratulations");
let string2 = "fantastic";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a> (x: &'a str, y: &'a str) -> &'a str {
let res = String::from("abc");
res.as_str()
}
上面的代码中,longest
函数中返回了局部变量 res
,当函数执行完毕时,局部变量 res
所指向的内存已经被释放掉,因此 test02
中的 result
变量指向的 res
内存已经被清理,这就造成了悬垂引用,非常类似于 C/C++
的野指针。
那么我就是想返回函数中的局部变量,应该怎么办呢?解决办法也很简单,就是直接返回这个值而不是返回引用,这样就将变量的所有权移交出去了,如下所示:
fn longest<'a> (x: &'a str, y: &'a str) -> String {
let res = String::from("abc");
res
}
因此从根本上讲,生命周期这种语法规则,是用来关联函数的不同参数及返回值之间的生命周期,一旦他们取得了某种联系,rust 就会获得足够的信息来支持保证内存安全的操作,并且阻止那些可能会导致悬垂指针或者其他违反内存安全的行为。
struct 里可以包括:
引用:需要在每个引用上添加生命周期标注
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn test04() {
let novel = String::from("Today is Tuesday. And I will take part in a meeting.");
let first_sentence = novel.split(".").next().expect("Can't find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
每个引用都有生命周期,需要为使用生命周期的函数或 struct 指定生命周期参数
但是下面这个例子,没有任何生命周期的标注,仍然可以通过编译:
fn first_word(s: &str) -> &str {
let byte = s.as_bytes();
for (i, &item) in byte.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
按照原来的 rust 规范,函数声明、参数和返回类型前都是要加上生命周期标注的,但是 rust 团队发现程序员总是一遍又一遍地标注同样的生命周期,而且这些场景是可以预测的,有着明确的模式,因此 rust 团队就将这些模式写入了编译器,使得借用检查器可以自动对这些模式进行推导而无需显式标注。
生命周期省略规则
生命周期在:
编译器使用三个规则在没有显式标注生命周期的情况下,来确定引用的生命周期
规则 1: 每个引用类型都有自己的生命周期
规则 2: 如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有输出生命周期参数
规则 3: 如果有多个输入生命周期参数,但其中一个是 &self
或 &mut self
,那么 self
的生命周期会被赋给所有的输出生命周期参数
生命周期省略的三个规则-例子
假设我们是编译器:
fn first_word(s: &str) -> &str {}
fn first_word<'a>(s: &'a str) -> &str {}
fn first_word<'a>(s: &'a str) -> &'a str {}
fn longest(x: &str, y: &str) -> &str {}
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {}
impl 块内的方法签名中
生命周期省略规则经常使得方法中的生命周期标注不是必须的
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> u32 {
3
}
fn printSome(&self, words: &str) -> &str {
println!("There are some words: {}", self.part);
self.part
}
}
'static
是一个特殊的生命周期:整个程序的持续时间
'static
生命周期let s: &'static str = "I have a static lifetime.";
'static
之前要三思
use std::fmt::Display;
fn longest_with_an announcement<'a,T>
(x: &'a str, y: &'a str, ann: T) -> 'a str
where
T: Display,
{
println! ("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
要注意的是,生命周期也是泛型的一种。