Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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,&selfannouncement 参数各自获得一个生命周期:

#![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
    }
}
}

这个函数的作用是返回两个字符串切片 xy 中较长的那个,但它现在多了一个参数 ann,代表 announcement。它的类型是泛型类型 T,而根据 where 中的约束,T 可以被替换为任何实现了 Display trait 的类型。