Wang's blog

十五、生命周期

Published on

生命周期是编译器的借用检查器用来保证所有借用都有效的一个概念。一个变量的生命周期从它被创建开始到它被销毁结束。

fn main() {
    let i = 3; // i生命周期开始 ─────────────────────────────┐
    //                                                     │
    { //                                                   │
        let borrow1 = &i; // 借用`borrow1`生命周期开始 ──────┐│
        //                                                ││
        println!("borrow1: {}", borrow1); //              ││
    } // `borrow1`生命周期结束 ─────────────────────────────┘│
    //                                                     │
    //                                                     │
    { //                                                   │
        let borrow2 = &i; // 借用`borrow2`生命周期开始 ──────┐│
        //                                                ││
        println!("borrow2: {}", borrow2); //              ││
    } // `borrow2`生命周期结束 ─────────────────────────────┘│
    //                                                     │
}   // i生命周期结束 ────────────────────────────────────────┘

显式标注

借用检查器使用显式生命周期标注来决定引用需要有效多久。

// foo具有一个生命周期参数'a,foo的生命周期不能超过'a
foo<'a>

// 一个带生命周期标注的类型
&'a T

// 使用多个生命周期参数,foo的生命周期不能超过'a或'b
foo<'a, 'b>

在函数中使用

带有生命周期的函数签名具有一些约束:

  • 任何引用必须带有生命周期标注
  • 任何返回的引用必须带有与某个输入相同的生命周期或为’static
  • 如果会导致返回无效数据的引用,则禁止在没有输入时返回引用
// 一个生命周期为'a的输入引用,它至少与函数的存活时间一样长
fn print_one<'a>(x: &'a i32) {
    println!("`print_one`: x is {}", x);
}

// 可以使用带生命周期的可变引用
fn add_one<'a>(x: &'a mut i32) {
    *x += 1;
}

// 多个具有不同生命周期的输入,这两个生命周期相同也是可以的
fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("`print_multi`: x is {}, y is {}", x, y);
}

// 可以返回输入的引用,但是必须带有正确的生命周期
fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }

// 错误,生命周期'a一定比函数长,生成的String在函数返回时被释放,导致指向无效数据的引用
fn invalid_output<'a>() -> &'a String { &String::from("foo") }

在方法中使用

与在函数中使用类似。

struct Owner(i32);

impl Owner {
    fn add_one<'a>(&'a mut self) { self.0 += 1; }
    fn print<'a>(&'a self) {
        println!("`print`: {}", self.0);
    }
}

在结构体中使用

与在函数中使用类似。

// 此结构体包含一个i32引用,该引用的生命周期必须比此结构体长
struct Borrowed<'a>(&'a i32);

// 类似的,这两个引用的生命周期必须比此结构体长
struct NamedBorrowed<'a> {
    x: &'a i32,
    y: &'a i32,
}

// 一个可以是i32也可以是i32引用的枚举
enum Either<'a> {
    Num(i32),
    Ref(&'a i32),
}

在特性中使用

与在函数中使用类似。注意impl也需要标注生命周期。

struct Borrowed<'a> {
    x: &'a i32,
}

impl<'a> Default for Borrowed<'a> {
    fn default() -> Self {
        Self {
            x: &10,
        }
    }
}

范围限制(bound)

类似泛型可以被限制范围,生命周期(本身就是泛型)也可以被限制范围。此处:的意义有所不同,但是+的意义相同。

  • T: ‘a:所有T类型引用的生命周期必须超过’a。
  • T: Trait + ‘a:T类型必须实现Trait特性并且所有T类型引用生命周期必须超过’a。
// 此结构体包含一个具有生命周期'a的T类型引用,T被限制为所有引用的生命周期必须超过'a。另外,此结构体的生命周期不能超过'a
#[derive(Debug)]
struct Ref<'a, T: 'a>(&'a T);

// 一个泛型函数,它使用Debug特性进行打印
fn print<T>(t: T) where
    T: Debug {
    println!("`print`: t is {:?}", t);
}

// 接收一个实现了Debug的类型T的引用,且所有T类型引用的生命周期超过'a。另外,'a必须超过函数的生命周期
fn print_ref<'a, T>(t: &'a T) where
    T: Debug + 'a {
    println!("`print_ref`: t is {:?}", t);
}

强制缩短(Coercion)

一个长生命周期可以强制缩短。这可以是由编译器推断出来的,也可以通过声明不同生命周期形成。

// 由于此函数要求两个变量具有相同生命周期,因此编译器会强制长生命周期变短
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}

// 此处显示声明'a至少与'b一样长,因此返回值生命周期会强制变短
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    // 具有较长生命周期
    let first = 2;
    {
        // 具有较短生命周期
        let second = 3;

        println!("The product is {}", multiply(&first, &second));
        println!("{} is the first", choose_first(&first, &second));
    };
}

静态生命周期

Rust有一些保留的生命周期,其中一个是静态生命周期’static,在两种情况下会遇到:

// 具有静态生命周期的引用
let s: &'static str = "hello world";

// 作为特性范围限制的一部分
fn generic<T>(x: T) where T: 'static {}

引用生命周期

静态生命周期’static表示引用指向的数据在整个程序的生命周期内都有效,但是它仍然可以被强制缩短。

有两种方法使一个变量具有’static生命周期,都是保存在程序的只读存储中:

  • 使用static声明一个常量
  • 声明一个类型为&‘static str的字符串字面值
// 使一个常量具有静态生命周期
static NUM: i32 = 18;

// 返回一个NUM的引用,使其生命周期缩短至与输入变量相同
fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}

{
    // 声明一个字符串字面值
    let static_string = "I'm in read-only memory";
    println!("static_string: {}", static_string);

    // 退出作用域后,static_string无法使用,但是其值仍然在二进制数据中
}

特性范围限制(trait bound)

使用’static作为特性范围限制,表示该类型不具有任何非静态生命周期的引用(即,接收者可以一直持有数据,不会失效)。需要理解自有数据总是具有’static生命周期,但是它们的引用没有。

// 只打印具有静态生命周期的数据
fn print_it( input: impl Debug + 'static ) {
    println!( "'static value passed in is: {:?}", input );
}

fn main() {
    // i是自有数据,所以具有'static生命周期
    let i = 5;
    print_it(i);

    // 错误,&i只有main()作用域定义的生命周期所有不是'static
    print_it(&i);
}

省略

一些非常常用的生命周期模式可以省略。

// 以下两个函数的生命周期签名完全相同
fn elided_input(x: &i32) {
    println!("`elided_input`: {}", x);
}
fn annotated_input<'a>(x: &'a i32) {
    println!("`annotated_input`: {}", x);
}

// 以下两个函数的生命周期签名完全相同
fn elided_pass(x: &i32) -> &i32 { x }
fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x }