生命周期为什么要提出、是什么、怎么用
导航
- 生命周期为什么要提出、是什么、怎么用
- 一、生命周期为什么要提出
- 二、生命周期是什么
- 三、生命周期怎么用
- 1、生命周期标注
- (1)引用类型标注
- (2)函数参数生命周期标注
- (3)结构体字段中生命周期标注
- (4)方法中生命周期声明
- (5)静态生命周期
- 2、赋值时生命周期规则
- 3、生命周期消除
一、生命周期为什么要提出
生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据
{
let r;
{
let x = 5;
r = &x;
}//x已失效
println!("r: {}", r);//r此时引用了一个无效数据,称r为悬垂引用,报错
}
二、生命周期是什么
通过生命周期的分析,确保所有权和借用的正确性,感觉可以说生命周期又是Rust本身的一种语言特性。。。。
三、生命周期怎么用
1、生命周期标注
生命周期标注通常是用在引用类型上的。
标注方式是‘+生命周期名称
(1)引用类型标注
&i32 // 一个引用
&'a i32 // 具有显式生命周期的引用,生命周期名称是a
&'a mut i32 // 具有显式生命周期的可变引用
(2)函数参数生命周期标注
fn useless<'a>(first: &'a i32, second: &'a i32) {}
函数名称后的<'a>只是生命周期名称声明,类似泛型也需要在函数名称后声明一样
上面的意思是,first参数和second参数的生命周期名称为‘a,这两个参数的实际生命周期是大于等于’a的
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
println!("x is {} and y is {}", x, y);
}
这段代码的意思是print_refs
有两个引用参数,它们的生命周期 'a
和 'b
至少得跟函数活得一样久
(3)结构体字段中生命周期标注
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
这段代码的意思是,结构体ImportantExcerpt
所引用的字符串 str
生命周期需要大于等于该结构体的生命周期,否则报错。
那你可能会问,字段引用怎么才会小于结构体的生命周期,那我们来看一个相关例子
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let i;
{
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
i = ImportantExcerpt {
part: first_sentence,
};
}// String::from("Call me Ishmael. Some years ago...")已失效
println!("{:?}",i);//i拥有的结构体中的字段指向失效的地址内容,也就是悬垂指针,报错
}
此时就会报错
error[E0597]: `novel` does not live long enough
--> src/main.rs:10:30
|
10 | let first_sentence = novel.split('.').next().expect("Could not find a '.'");
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
14 | }
| - `novel` dropped here while still borrowed
15 | println!("{:?}",i);
| - borrow later used here
(4)方法中生命周期声明
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,//约束,b生命周期要小于a的生命周期
{
println!("Attention please: {}", announcement);
self.part
}
}
(5)静态生命周期
let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
- 生命周期 'static 意味着能和程序活得一样久,例如字符串字面量和特征对象
- 实在遇到解决不了的生命周期标注问题,可以尝试 T:'static,有时候它会给你奇迹
2、赋值时生命周期规则
fn longest<'a:'b,'b>(x: &'a str, y: &'b str) -> &'b str {
if x.len() > y.len() {
x
} else {
y
}
}
在返回值有多个 不同生命周期 的值返回时,要求返回类型是 所有返回值中 生命周期最小的那个。
这里有一个生命周期赋值的基本原则
短命的才可以引用长命的,长命的引用短命的,当长命的想使用短命的,万一短命的已经挂了,那就发生悬挂引用现象,导致报错。
声明生命周期之间的长短关系,以上面的函数longest
为例子,'a:'b
表示为,在’a、'b两个生命周期中,'b的生命周期小于’a的生命周期。
那么在函数体内,无论返回x还是y,返回类型的生命周期至少都是小于或等于x和y的,满足借用安全,不会发生悬垂引用
3、生命周期消除
实际上,对于编译器来说,每一个引用类型都有一个生命周期,那么为什么我们在使用过程中,很多时候无需标注生命周期?例如
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
该函数的参数和返回值都是引用类型,尽管我们没有显式的为其标注生命周期,编译依然可以通过。其实原因不复杂,编译器为了简化用户的使用,运用了生命周期消除大法。
减少了程序员的工作量
但是在生命周期消除不是万能的,需要注意的是
- 消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期
- 函数或者方法中,输入参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期
然后生命周期消除主要遵循一下三个规则
- 每一个引用参数都会获得独自的生命周期
例如一个引用参数的函数就有一个生命周期标注: fn foo<'a>(x: &'a i32),两个引用参数的有两个生命周期标注:fn
foo<'a, 'b>(x: &'a i32, y: &'b i32), 依此类推
- 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期
例如函数 fn foo(x: &i32) -> &i32,x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn
foo<'a>(x: &'a i32) -> &'a i32
- 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期
拥有 &self 形式的参数,说明该函数是一个 方法,该规则让方法的使用便利度大幅提升。