前言#
昨天我们学了trait
今天我们继续往下学,该学生命周期了。
生命周期(lifetime)#
lifetime也是之前mark了很久的点,今天总算见到真面目了。
之前我们在学第四章《References and Borrowing》[2] 的时候只是简单的提了一下。实际上任何的引用都有一个生命周期,它表示这个引用的有效范围,超出这个范围就挂了。
和type的自动推断类似(大部分情况下),生命周期大部分情况下也是自动推断出来的。
当然,也有一些场景需要特别对待。
rust要求我们用通用的lifetime parameter去注释(annotate)关系(relationship),这样能确保运行时的引用绝对有效。
这句话前半段有点怪。。。
这玩意儿在别的语言中很难说有或者类似的,有点陌生呢。。
避免悬浮(dangling)引用#
还记得第四章学的悬浮引用吗?

r引用了x,但是x在第八行就挂了,这个时候r引用的自然就不对了。
用今天学的知识来说就是x的生命周期在第八行就结束了。
rust中是如何推测它不允许这么做的呢?
借用检查器(borrow checker)#
rust在编译的时候会调用这个borrow checker,它会对比作用域来推断(determine)是否所有引用都是有效的(valid)。
比如上面的那个例子
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+r的生命周期是'a,而x是'b,可以看到'a的生命周期远大于'b。
在r借用x的时候,borrow checker就会比对它们的生命周期范围。
上面的代码改成下面这种方式就没问题了
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
可以看到这个时候x的生命周期明显大于r的。
这也就是说,只有被引用的对象生命周期大于等于引用它的变量时,就没问题。反之则编译报错。
在函数里使用引用参数#
我们先来看下例子
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
}
}
可以看到报错了,虽然x和y的类型都是一样的&str,也就是返回的值类型已经确定是&str一种,符合要求。
为什么报错了呢?
因为rust并不知道你这个引用是否是一直有效的。
这个函数在哪里会被调用,传入的引用会在什么时候挂掉,这些都是不确定的。
那么该怎么解决这个问题呢?
生命周期注释语法(lifetime annotation syntax)#
在rust中使用'a,表示一个生命周期,比如以下用法
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime得放在&引用符号后面。
当然,用'后面的符号你想用啥字母都行。不过一般都是单个字符,因为它的名字一般不需要有意义,只是告诉rust编译器这个参数关联的是哪个生命周期,一般都是'a表示第一个引用的生命周期。
在函数中使用生命周期注释(annotation)#
上面那个问题我们直接来看下解决的办法
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
}
}注释在函数名和()之间,也就是和T泛型的定义位置是一样的。
这样,x和y的生命周期就被约束为一样了,同时由于你return的value也约束了'a的生命周期,所以它的有效范围和x,y的一致。
简单地说,就是x,y以及return的value都是类型一样,生命周期一样,那么在这个方法中compiler就能确定它没问题了。
当然,如果你传入的参数的生命周期不一样,那还是会报错的,不过这个错误是写代码这人的错误了(来自borrow checker的审判)。
在你注释生命周期之后,rust只能帮你确认是否是相同作用域。
但如果你不声明,对不起,一定会报错。
可以看到和泛型一样(大部分),我们在调用的时候并不需要去手动传入,只要函数声明了参数的生命周期,编译器就会自动判断参数的上下文环境,然后推断出它们的命有多长。
来看下上面的例子不同作用域的情况
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
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
}
}
可以看到并没有报错。
为啥呢?
虽然x和y并不在同一个作用域,但是因为return的value是在最小的那个生命周期里,同时result所在的作用域是最小的作用域,所以并没有啥问题。那么这个时候'a生命周期范围就是最小的那个。
那么我们再来改动下代码
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
我们把result放到main这个作用域中,果然报错了。
因为x和y不在同一个作用域并且result所在作用域和'a不是同一个,那么这时候再将最小作用域当做生命周期范围就会有问题了,因为超出这个范围之后result还被调用了。
这时只需要把println!的语句迁移回最小作用域就没问题了。
这里你应该已经发现了,生命周期注释只是通知编译器这些个参数的命有多长,然后编译器调用borrow checker帮你去判断这个参数能不能被雇佣。
从生命周期的角度去思考#
要不要用到生命周期,完全取决于你这个function是要做什么。
如果你这个函数里的参数并不和return的value或者其它任何参数有关联,那完全没必要去注释这个参数的生命周期。
不过上面这种情况可能会导致下面这种问题

也就是没有任何参数需要注释生命周期,这种时候如果你返回一个引用,那么这个引用一定会是一个悬浮引用,因为它的作用域就在函数里面,函数出调用栈的时候它就失效了。
即使我们注释了'a这个生命周期也是没有用的,为什么呢?因为压根没有和'a所在的作用域有生命周期关联。
这种问题完全就是开发者自己的问题了,rust表示无能为力~
所以你只能改成返回这个值的所有权。
所以,要不要用到生命周期注释完全取决于你这个function想干啥。
rust搞了这套理论就是为了保证引用的正确性,防止引用指向错误的heap内存空间。
在结构体中使用生命周期#
我们之前提到生命周期的地方不止一处,在学strcut的时候就提到过。
为什么struct中也有这种问题呢?
因为struct可以不拥有数据的所有权。
来看下下面这个例子
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,
};
}这段代码是完全没问题的,但是去掉'a就会有问题了。和在function中是一样的道理,rust的编译器并不知道你这个struct里的part属性啥地方会被调用。

规范和T泛型(generic)一样。
省略生命周期#
现在我们知道了function或者struct中使用引用参数并且和return value有染(误),那么这个时候大概率你就需要去声明生命周期。
但是,除了没有关联的场景外, 有些场景实际上没必要写引用。
比如:
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 main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}first_word这个method就是使用了引用参数s并且返回的数据和它有染。但是编译器并没有报错。为啥呢?
实际上这是一个历史问题,在rust的早期版本pre-1.0中,上面这段代码同样是会报错无法通过编译的。
但是,在一段时间的使用之后,rust团队发现开发者们在某些特定的场景会一遍又一遍输入同一个生命周期,比如上面这个例子
fn first_word<'a>(s: &'a str) -> &'a str {开发者们每次都要去输入它们,就很烦,并且它们实际上是可以被推断出来的。
所以rust开发团队把这部分可以直接推断出来的规则写进了compiler中,这样开发者们就不用每次都去声明了。将来也有可能会加入更多的推断逻辑,这样就能减少更多的生命周期注释了。
rust团队称其为*lifetime elision rules 。*
如果你的代码符合了里面的其中一种规则,那你完全没必要去注释生命周期了,毕竟这样有些麻烦。
当然,如果你的代码写的含糊不清,即使符合了规则,编译器无法识别也会报错。
rust`团队把注释在`function/method`(是的,`impl`块里的`method`也是符合这些规则的)里的`parameter`的生命周期叫做`*input lifetimes*而return出去的叫做*output lifetimes.*
如果你没有注释生命周期的话,rust编译器会基于三条规则(三板斧)推断生命周期。也就是上面那个被插入到compiler里的自动推测规则(lifetime elision rules)。
第一条用于input lifetimes,另外两条用于output lifetimes。
如果三板斧都用完,还是没法推断出来,那对不起,直接报错处理。
- 你的引用参数有几个就有几个生命周期注释,比如:
fn foo<'a, 'b>(x: &'a i32, y: &'b i32);- 如果只有一个引用参数,那么这个参数的生命周期将被分配给所有的
output lifetimes也就是return,比如:
fn foo<'a>(x: &'a i32) -> &'a i32 - 如果有多个引用参数,但是其中一个是
&self或者&mut self,也就是说是一个method,那么这个时候self的生命周期将被分配给所有output lifetimes。
我们来模拟下编译器处理这块的逻辑,例子就用下面这个
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[..]
} 毕竟它是一个命中规则的function,所以用它再好不过了。
根据第一板斧,它将被转换成下面这个样子
fn first_word<'a>(s: &'a str) -> &str {
//...
} 先给所有input分配一个lifetime注释。
然后根据第二板斧,我们只有一个input引用参数,那么这个参数的生命周期将被分配给output
fn first_word<'a>(s: &'a str) -> &'a str {
// ...
}这样就ok了,第三板斧并不需要用到,毕竟function没有self。
我们再来换个例子
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}根据第一板斧,将变成
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// ...
}多input,多lifetime annotation。
然后根据第二板斧,不适用。
同样的第三板斧更不适用了,那么这个时候打不过只能跑了,直接报错处理。
我们再来看个例子,关于第三板斧的。
在method声明中使用生命周期注释#
和泛型的使用一样,直接来看下例子
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
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,
};
}我们定义了一个含有引用类型的struct,然后又给这个struct实现了两个method。
其中level方法返回的数据是一个i32类型的,也就是说完全没有引用关联。可以看到我们的&self,而不是&'a self,因为没必要。但是impl的'a是不能省略的,会直接报错

是不允许的。
而announce_and_return_part就符合我们的第三板斧,所以我们也没必要写,这里也不会报错。那么为什么这第三板斧不需要写注释呢?
其实这块逻辑挺好想通的,因为你的method只能是这样调用struct_instance.xx()。所以只要struct_instance在哪,这个return value也就在哪。
静态的生命周期#
什么情况下可以用呢?比如字面量。
#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}关键字眼'static,为啥它是static的呢?
因为这玩意儿直接hardcode也就是硬编码到binary里,所以这玩意儿会一直有效。
当然,大部分情况下你都不会用到。不过还是有的,比如
fn main () {
let s = test();
println!("{s}");
}
fn test () -> &'static str {
"test"
}同时使用泛型(generic)、trait bound和lifetimes#
第十章终于结束了。。。。
我们来将学的东西拼接到一起试下
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest_with_an_announcement(
string1.as_str(),
string2,
"Today is someone's birthday!",
);
println!("The longest string is {}", result);
}
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
}
}没什么好说的,直接看例子即可。
总结#
Validating References with Lifetimesdoc.rust-lang.org/book/ch10-03-lifetime-syntax.html#summary
不知道说啥,自己看吧。。
好累、好冷
昨天吃了华莱士,肚子疼了一天。
然后又这么冷,搞得什么都不想干。
最后,如果这篇文章对你有帮助的话,点个赞可否?
参考#
- ^rust-lifetime https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#validating-references-with-lifetimes
- ^rust-references-and-borrowing https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#references-and-borrowing
- ^rust-use-lifetime-to-prevent-dangling-reference https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#preventing-dangling-references-with-lifetimes
- ^rust-borrow-checker https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-borrow-checker
- ^rust-generic-lifetimes-in-functions https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#generic-lifetimes-in-functions
- ^rust-lifetime-annotation-syntax https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
- ^rust-lifetime-annotation-in-function-signatures https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotations-in-function-signatures
- ^rust-thinking-in-terms-of-lifetimes https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#thinking-in-terms-of-lifetimes
- ^rust-use-lifetime-in-struct https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotations-in-struct-definitions
- ^rust-lifetime-elision https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-elision
- ^rust-lifetime-annotation-in-method-definitions https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotations-in-method-definitions
- ^rust-static-lifetimes https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
- ^use-generic-trait-bound-and-lifetimes-together https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#generic-type-parameters-trait-bounds-and-lifetimes-together
发布于 2022-12-18 01:28・IP 属地广东
