所有权系统(2)

112 阅读5分钟

生命周期参数

值的生命周期和词法作用域有关,但是借用可以在各个函数间传递,必然会跨越多个词法作用域。

如果只是在函数本地使用借用,那么借用检查器很容易推到其生命周期,因为此时 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> {}