🍉 加载中...


Rust 生命周期注解笔记 📒

8 minute read

通常情况下,我们需要为那些使用了引用的函数结构体指定生命周期。

前置知识

  • 函数或方法入参的生命周期被称为输入生命周期(input lifetimes),而返回值的生命周期被称为输出生命周期(output lifetimes)
  • 生命周期注解只是用于供借用检查器使用,注解本身无法改变任何生命周期。

函数中的生命周期注解

在函数签名中指定生命周期参数时,并没有改变任何入参或返回值的生命周期,而是明确告知借用检查器拒绝任何不满足这个约束条件的值。

以下示例中,函数签名表达的意思(限制)是:对于某些生命周期 ‘a,函数会获取两个参数,他们都是与生命周期 ‘a 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 ‘a 存在的一样长的字符串 slice。它的实际含义是 longest 函数的返回值的引用的生命周期,与函数入参所引用的值的生命周期的较小者一致。

1fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
2    if x.len() > y.len() {
3        x
4    } else {
5        y
6    }
7}

当具体的引用被传递给 longest 时,被 ‘a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 ‘a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。 因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值能够保证在 x 和 y 中较短的那个生命周期结束之前保持有效。

通过下面两个示例,可以很明确的看出这一点:

 1// 示例 A
 2fn main() {
 3    let string1 = String::from("long string is long");
 4
 5    {
 6        let string2 = String::from("xyz");
 7        let result = longest(string1.as_str(), string2.as_str());
 8        println!("The longest string is {}", result);
 9    }
10}
 1// 示例 B
 2fn main() {
 3    let string1 = String::from("long string is long");
 4    let result;
 5
 6    {
 7        let string2 = String::from("xyz");
 8        result = longest(string1.as_str(), string2.as_str());
 9    }
10
11    println!("The longest string is {}", result);
12}

示例 B 是无法通过编译的,因为 result 的生命周期超出了 string2 的生命周期。

拓展

以下示例中,longest 函数的实现总是返回第一个参数,因此不需要为参数 y 指定生命周期注解。

1fn longest<'a>(x: &'a str, y: &str) -> &'a str {
2    x
3}

结构体中的生命周期注解

定义包含引用的结构体时,需要为结构体定义中的每一个引用添加生命周期注解。

以下示例中的生命周期注解,意味着 ImportantExcerpt 的实例的存活周期不能超过 part 字段中的引用的存活周期

 1struct ImportantExcerpt<'a> {
 2    part: &'a str,
 3}
 4
 5fn main() {
 6    let novel = String::from("Call me Ishmael. Some years ago...");
 7    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
 8
 9    let i = ImportantExcerpt {
10        part: first_sentence,
11    };
12}

方法中的生命周期注解

1impl<'a> ImportantExcerpt<'a> {
2    fn announce_and_return_part(&self, announcement: &str) -> &str {
3        println!("Attention please: {}", announcement);
4        self.part
5    }
6}

静态生命周期注解

有一种特殊的生命周期值得讨论:'static,其生命周期覆盖了程序的整个运行周期(从启动到退出)。

所有的字符串字面值都拥有 'static 生命周期。例如下面示例中,字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 'static 的。

1let s: &'static str = "I have a static lifetime.";

大部分情况,推荐 'static 生命周期的错误信息都是在尝试创建一个悬垂引用或者可用的生命周期不匹配的结果。

生命周期省略规则(lifetime elision rules)

编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块

三条规则如下:

  • 编译器会单独为每个引用参数分配一个生命周期注解。换句话说就是,有一个引用参数的函数有一个生命周期注解:fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期注解,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。
  • 如果只有一个输入生命周期参数,那么它将被赋给所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32
  • 如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法,那么所有输出生命周期参数都被赋予 self 的生命周期。

示例

 1fn first_word(s: &str) -> &str {
 2    let bytes = s.as_bytes();
 3
 4    for (i, &item) in bytes.iter().enumerate() {
 5        if item == b' ' {
 6            return &s[0..i];
 7        }
 8    }
 9
10    &s[..]
11}

终末笔记

生命周期注解是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

函数与结构体中的生命周期注解作用存在差异,结构体中的生命周期注解用于约束结构体的生命周期,而函数中的生命周期注解则是用于约束函数返回值的生命周期。

参考资料