生命周期参数
值的生命周期和词法作用域有关,但是借用可以在各个函数间传递,必然会跨越多个词法作用域。
如果只是在函数本地使用借用,那么借用检查器很容易推到其生命周期,因为此时 Rust 拥有关于此函数的所有信息。
一旦跨函数使用借用,比如作为函数参数或返回值使用,编译器就无法进行检查,因为编译器无法判断所有的传入或传出的借用生命周期范围,此时需要显式地对借用参数或返回值使用生命周期参数进行标注。
显式生命周期参数
生命周期参数必须以单引号开头,参数名通常都是小写字母,比如 'a 。
生命周期参数位于引用符号 & 后面,并使用空格来分割生命周期参数 和 类型,如下所示:
&i32 // 引用
&'a i32 // 标注生命周期参数的引用
&'a mut i32 // 标注生命周期参数的可变引用
标注生命周期并不能改变任何引用的生命周期长短,它只能用于编译器的借用检查。
函数签名中的生命周期参数
函数签名中的生命周期参数使用如下标注语法:
fn foo<'a>(s: &'a str, t: &'a str) -> &'a str;
函数名后面的 <'a> 为生命周期参数的声明,与泛型参数类似,必须先声明才能使用。
函数或方法参数的生命周期叫做 输入生命周期(input lifetime),而返回值的生命周期被称为 输出生命周期(output lifetime)。
限制条件:
-
输出(借用方)的生命周期长度必须不长于输入(出借方)的生命周期长度。(借用规则一)
-
禁止在没有任何输入参数的情况下返回引用。
-
从函数中返回(输出)一个引用,其生命周期参数必须与函数的(输入)相匹配,否则,标注生命周期参数也毫无意义。
fn main() { let s1 = "aaa"; let s2 = "bbbb"; let re = the_longest(s1, s2); println!("re = {}", re); let re = the_longest2(s1, s2); println!("re = {}", re); } // 返回借用,编译器无法推导出返回的借用是否合法,代码示例: fn the_longest<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str { if s1.len() > s2.len() { return s1; } return s2; } // 标注多个生命周期参数的示例: fn the_longest2<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str { if s1.len() > s2.len() { return s1; } return s2; }
生命周期参数的目的: 生命周期参数是为了帮助借用检查器验证非法借用,函数间传入和返回的借用必须相关联,并且返回的借用生命周期一定不能长于出借方的生命周期。
结构体定义中的生命周期参数
结构体在含有引用类型成员的时候也需要标注生命周期参数,否则编译器会报错: missing lifetime specifier。
fn main() {
let words = Foo { part: "hello foo!" };
assert_eq!(words.part, "hello foo!");
}
struct Foo<'a> {
part: &'a str,
}
示例中的生命周期参数标记,实际上是和编译器约定了一个规则:结构体实例的生命周期应短于或等于任意一个成员的生命周期。
方法定义中的声明周期参数
匿名生命周期样例:
fn main() {
let words = Foo { part: "hello foo!" };
assert_eq!(words.part, "hello foo!");
let re = Foo::split_first("hello, world");
println!("split_first = {}", re);
println!("parts_first = {}", words.parts_first());
}
struct Foo<'a> {
part: &'a str,
}
impl Foo<'_> {
fn split_first(s: &str) -> &str {
s.split(",").next().expect("Could not find a ','")
}
fn parts_first(&self) -> &str {
self.part.split(",").next().expect("Could not find")
}
}
静态生命周期参数
Rust 内置了一种特殊的生命周期 'static,叫做静态生命周期。
'static 生命周期存活于整个程序运行期间。
所有的字符串字面量都有 'static 生命周期,类型为 &'static str。
在 Rust 2018 版本中,使用 const 和 static 定义字符串字面量时,都可以省掉 'static 静态生命周期参数。
省略生命周期参数
对于理论上需要显式地标注生命周期参数的情况,实际中依然存在可以省略生命周期参数的可能。这是因为 Rust 针对某些场景确定了一些常见的模式,将其硬编码到 Rust 编译器中,以便编译器可以自动补齐函数签名中的生命周期参数。
被硬编码进编译器的模式称为 生命周期省略规则(Lifetime Elision Rule),一共包含三条原则:
- 每个输入位置上省略的生命周期都将成为一个不同的生命周期参数。
- 如果只有一个输入生命周期的位置(不管是否忽略),则该生命周期将分配给输出生命周期。
- 如果存在多个输入生命周期的位置,但是其中包含着 &self 或 &mut self,则 self 的生命周期将分配给输出生命周期。
生命周期限定
生命周期参数可以像 trait 那样作为泛型的限定,有一下两种形式。
- T: 'a,表示 T 类型中的任何引用都要 “活得” 和 'a 一样长。
- T: Trait + 'a,表示 T 类型必须实现 Trait 这个 trait,并且 T 类型中任何引用都要 “活得” 和 'a 一样长。
trait 对象的生命周期
如果一个 trait 对象中实现 trait 的类型带有生命周期参数,如何处理? 示例如下:
use std::fmt::Debug;
fn main() {
let s1 = "hello";
let box_bar = Box::new(Bar { x: s1 });
let obj = box_bar as Box<dyn Foo>;
println!("obj = {:?}", obj);
}
trait Foo: Debug {}
#[derive(Debug)]
struct Bar<'a> {
x: &'a str,
}
impl<'a> Foo for Bar<'a> {}