10.8 生命周期 Pt.4:方法定义中的生命周期标注与静态生命周期
10.8.1. 方法定义中的生命周期标注
还记得上一篇文章 10.7. 输入输出生命周期与3规则 中提到的三条生命周期省略规则吗?
规则 1: 每个引用参数都会获得自己的生命周期。 单参数函数有一个生命周期,双参数函数有两个生命周期,以此类推。
规则 2: 如果恰好只有一个输入生命周期参数,那么该生命周期会被赋给所有输出生命周期参数。 换句话说,如果只有一个输入生命周期,那么这个生命周期就是该函数所有可能返回值的生命周期。
规则 3: 如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self(也就是说,这个函数是方法),那么 self 的生命周期会被赋给所有输出生命周期参数。
在 10.7. 输入输出生命周期与3规则 的例子中,我们应用了规则 1 和 2,但没有应用规则 3,因为规则 3 只适用于方法。所以这里要讲的就是规则 3,也就是方法定义中的生命周期标注。
方法需要一个结构体,而在结构体上使用生命周期来定义方法,其写法与泛型参数相同(见 10.7. 输入输出生命周期与3规则)。
在哪里声明和使用生命周期参数,取决于该生命周期参数是否与字段、方法参数或返回值有关。
结构体字段的生命周期名总是声明在 impl 关键字后面,然后在结构体名称后面使用,因为这些生命周期是结构体类型本身的一部分。
在 impl 块内的方法签名中,引用必须绑定到结构体字段引用的生命周期,也可以是独立的。此外,生命周期省略规则常常使方法中的生命周期标注变得不必要。
多说无益,看个例子:
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
首先定义了 ImportantExcerpt 结构体,然后为它定义了 level 方法。level 方法只接收 &self 作为参数,返回值是 i32,因此它没有引用任何东西。
“结构体字段的生命周期名总是声明在 impl 关键字后面,然后在结构体名称后面使用”这句话,指的就是第 4 行在 impl 后面写了 <'a>,在结构体名 ImportantExcerpt 后面也写了 <'a>。
注意,第 4 行的两个 <'a> 标注都不能省略,但 level 函数不需要在 &self 上加生命周期标注,因为适用了生命周期省略规则 1 和 2。
现在再添加一个方法:
#![allow(unused)]
fn main() {
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
}
根据生命周期省略规则 1,&self 和 announcement 参数各自获得一个生命周期:
#![allow(unused)]
fn main() {
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'a, 'b>(&'a self, announcement: &'b str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
}
根据生命周期省略规则 3,返回值被赋予与 &self 相同的生命周期:
#![allow(unused)]
fn main() {
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'a, 'b>(&'a self, announcement: &'b str) -> &'a str {
println!("Attention please: {announcement}");
self.part
}
}
}
至此,所有生命周期都已被推断出来,因此编译器可以成功编译代码。
10.8.2. 'static 生命周期
Rust 有一个特殊的生命周期叫做 'static,它表示整个程序的持续时间,或者说整个程序的执行时间。
例如,所有字符串字面值都拥有 'static 生命周期,比如:
#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}
这是一个字符串字面值,因此可以用 'static 标注。
字符串字面值拥有 'static 生命周期的原因是:它们被直接存储在二进制文件中,并在运行时放在静态内存中,因此总是可用。
在给普通引用指定 'static——编译器报错时经常会这样建议——之前,你必须仔细思考:你是否真的需要这个引用在整个程序持续期间都存活? 更大的可能是,编译器报错是因为悬空引用或生命周期不匹配。这时,你应该尝试解决这些问题,而不是简单地给它贴上一个 'static 生命周期。
10.8.3. 泛型类型参数、trait bound 与生命周期
最后,看一个同时使用了泛型类型参数、trait bound 和生命周期的例子:
#![allow(unused)]
fn main() {
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
}
}
}
这个函数的作用是返回两个字符串切片 x 和 y 中较长的那个,但它现在多了一个参数 ann,代表 announcement。它的类型是泛型类型 T,而根据 where 中的约束,T 可以被替换为任何实现了 Display trait 的类型。