跳转至

生命周期

一、什么是生命周期?

Rust的每个引用都有生命周期,生命周期是可以理解是引用保持有效的作用域。大多数情况下,生命周期是隐式的、可被推断的。当引用的生命周期可能以不同的方式相互关联的时候,必须手动标注生命周期。

二、借用检查器

生命周期的主要目标是避免垂悬引用(dangling reference),如下示例的错误代码

fn main() {
    {
        let r;
        {
            let x = 5;
            r = &x;
        }
        println!("r: {}", r);
    }
}

在最里层的大括号中,r指向了x,但是在最里层括号的外面,调用了println!,编译会报错,因为在最里层括号外面,x已经被释放了。rust为了保证程序的安全性,任何基于r的操作无法正常运行。

Rust编译器的借用检查器,用来比较引用的作用域从而判断所有的借用是否合法。如下加上注释的示例代码

fn main() {
    {
        let r;                  // ------+-- 'a
        {                       //
            let x = 5;          // ------+- 'b
            r = &x;             // 
        }                       // ------+- 'b
        println!("r: {}", r);   //
    }                           // ------+-- 'a
}

可以看到x的生命周期比它的引用者r要短,所以rust不允许编译通过。要解决这个问题,必须让x的生命周期不小于r的生命周期。如下示例代码

fn main() {
    let x = 5;
    let r = &x;

    println!("r: {}", r);
}

在以上代码中,x的生命周期完全覆盖了它的调用者r的生命周期,所以能顺利通过编译。

三、函数中的泛型生命周期

我们先看下面一段错误的代码

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_string(), string2);

    println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在以上代码中,longest函数的目的是返回两个字符串中比较长的值,并在main函数中打印出来。传入参数为xy两个字符串切片,返回值也是字符串切片。xy的生命周期可能不一样,而返回的值生命周期应当是确定的,但是函数签名里返回的值不知道借用自x还是y,所以代码编译时会产生错误。

我们先给出一个解决方案,如下示例代码

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在以上代码中'a代表生命周期泛型,同时指定了xy的生命周期为'a,同时返回的值生命周期也为'a,此时代码正常通过编译。不过,如果我们在这里说xy和返回的值的生命周期是一样的,这种说法是不太准确的。在这里,'a被叫做生命周期标注,在下文我们进一步探讨此概念。

四、生命周期标注语法

生命周期的标注不会改变引用的生命周期长度,当某个函数指定了泛型生命周期参数,函数可以接受带有生命周期的引用。生命周期的标注描述了多个引用的生命周期的关系,但不影响生命周期。

参数周期标注,遵循以下语法规则

  • 参数名:以'开头;使用全小写且非常短,很多人使用'a

  • 标注的位置:在引用的&符号后;使用空格将标注和引用类型分开

生命周期标注例子如下

  • &i32:一个普通引用
  • &'a i32:带有显式生命周期的引用
  • &'a mut i32:带有显式生命周期的可变引用

单个生命周期标注本省没有意义,当多个参数使用同一的标注时,代表多个参数必须拥有一样的生命周期。泛型生命周期参数声明在函数名和参数列表之间的<>里。到这里,我们在回顾*三*中的代码

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

代码解读如下

  • longest函数声明了一个'a生命周期
  • xx的生命周期必须大于等于'a
  • 返回值的生命周期也必须大于等于'a

当我们在函数体里面指明生命周期参数的时候,我们并没有改变传入的值和返回的值的生命周期,我们只是向*借用检查器*指出了一些用于检查非法调用的一些=约束而已。在上面的例子中,longest函数本身并不需要知道xy具体的生命周期,而只需要某个作用域可以被用来代替'a,同时满足函数的签名约束即可。

如果函数引用外部的代码或者被外部代码引用的时候,想单靠rust本身来确定参数和返回值的生命周期几乎是不可能的,这样的话函数所使用的生命周期可能在每次调用中都会发生变化,正式因此我们才需要手动对生命周期进行标注。

示例代码中的'a实际上代表xy重叠的那部分生命周期(也就是xy两个生命周期中较小的生命周期),而返回的值则在xy重叠的那部分生命周期是有效的。

五、深入理解生命周期

指定生命周期参数的方式依赖于函数所做的事情。当函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配。

如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值,该值在函数结束时就走出了作用域。如下示例代码的错误代码

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_string(), string2);

    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    let result = String::from("abc");
    result.as_str()
}

longest函数中,result.as_str()的结果是字符串切片,其实就是一个引用。此时编译将会出错,因为result.as_str()返回的值是引用longest函数内部所持有的数据。当内部数据离开作用域的时候,函数返回的引用仍然指向数据存储的内存,返回的引用给main函数中的result,并在main函数中进行使用,而该引用指向的内存,在longest之行结束的时候已经被释放了,这就发生了悬垂引用。rust不会允许此类不安全的操作,所以编译不通过。

如果我们想把函数内部的值直接返回出去,可以改成直接返回值,而不是返回引入,修改如下

fn longest<'a>(x: &'a str, y: &'a str) -> String {
    let result = String::from("abc");
    result
}

此时相当于把所有权移交给函数的调用者,如果想清理内存,由函数的调用者来清理即可。

从根本上讲,生命周期语法是用来关联函数的不同参数以及返回值之间的生命周期,一旦它们之间取得了某种联系,rust将获得足够的信息来支持保证内存安全的操作,并且阻止那些可能会导致垂悬指针或者其他违反内存安全的行为。

六、Struct定义中的生命周期标注

Struct里可以包括自持有类型(如i32等)和引用类型,如果是引用类型,需要在每个引用上添加生命周期标注。结构体的引用字段,生命周期必须比结构体本身长。如下示例代码

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel String::from("Call me Ishmael. Some years ago...");

    // 下面一行first_sentence的生命周期开始
    let first_sentence = novel.split('.').next().expect("Could not found a '.'");

    // 下面一行i的生命周期开始
    let i = ImportantExcerpt {
        part: first_sentence
    };
}

在以上代码中,可以看到字符切片引用的生命周期比struct ImportantExcerpt的生命周期长,所以编译通过。

七、生命周期的省略

7.1 生命周期省略规则

我们知道,每个引用都有生命周期,需要为使用生命周期的函数或struct指定生命周期参数。我们先来看一段在本系列切片代码

fn first_world(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

在上面的函数中,返回的是一个字符串切片,但是没有标注任何生命周期,而仍然可以通过编译。这就涉及到rust语言发展的历史了,在rust的早期版本,这段代码是无法编译通过的,因为当时要求每个引用都必须有一个显式的生命周期。如下示例代码

fn first_world<'a>(s: &'a str) -> &'a str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

在编写很多代码之后,rust团队发现,在某些特定的情况下,rust程序员总是一遍又一遍地编译同样的生命周期标识,而且这些场景是可以预测的,而且有一些明确的模式,于是rust团队直接将这些模式写入了编译器代码,使得借用检查器在这些情况下可以自动地对生命周期进行推倒而无需显式地标注。了解这段历史很有必要,因为在未来可能有更多的模式添加到编译器中,也就是说在未来手动标注生命周期的情况可能会越来越少。

在Rust引用分析中所编入的模式成为**生命周期省略规则**,这些规则无需开发者来遵循,它们是一些特殊情况,由编译器来考虑,如果你的代码符合这些情况,那么就无需显式标注生命周期。

**生命周期省略规则**不会提供完整的推断,如果应用规则后,引用的生命周期仍然模糊不清,那边编译不会继续推断,将会产生错误。这时候的解决办法是:需要手动添加生命周期标注,表明引用间的相互关系。

7.2 输入、输出生命周期

如果生命周期出现在函数/方法的**参数**中,那么这个生命周期就叫输入生命周期;如果生命周期出现在函数/方法的**返回值**中,就叫输出生命周期。

7.3 生命周期省略的三个规则

7.3.1 规则定义

编译器使用3个规则在没有显示标注生命周期的情况下,来确定引用的生命周期。规则1应用于输入生命周期,规则2、3应用于输出生命周期。如果编译器应用完3个规则后,仍然无法确定生命周期的引用,则编译器会报错。这些规则适用于fn定义和impl块。

  • 规则1: 每个应用类型的参数都有自己的生命周期,换句话说,单参数的函数它拥有一个生命周期参数,拥有两个参数的函数就拥有两个不同的生命周期参数,以此类推;

  • 规则2: 如果只有一个输入生命周期参数,那么该生命周期被赋给所有的生命周期参数;

  • 规则3: 如果有多个输入生命周期参数,但其中一个是&self&mut self(是方法),那么self的生命周期会被赋予给所有的输出生命周期参数。

7.3.2 生命周期省略规则示例1

下面是第一个示例函数

fn first_world(s: &str) -> &str {
    // .....
}

根据第一个规则,rust编译器会为每个输入参数增加一个生命周期,此时变成如下代码

fn first_world<'a>(s: &'a str) -> &str {
    // .....
}

因为该函数只有一个输入生命周期参数时,这个生命周期被会赋给所有输出生命周期参数,此时变成如下代码

fn first_world<'a>(s: &'a str) -> &'a str {
    // .....
}

现在所有的引用都有了生命周期,所以编译器可以继续分析代码,而无需程序员手动标注这个函数签名里的生命周期了。

7.3.3 生命周期省略规则示例2

首先看下面的函数签名

fn longest(x: &str, y: &str) -> str {
    // ......
}

上面的函数中,适用第1个规则,每个参数都有自己的生命周期,此时变成如下代码

fn longest(x: &'a str, y: &'b str) -> str {
    // ......
}

由于现在这个函数拥有两个输入生命周期,所以第2条规则不适用;而且由于longest是个函数不是方法,没有self参数,所以第3条规则也不适用。所以应用完这三条规则之后,我们仍然无法计算返回类型的生命周期,所以编译器会报错。也就是说,当编译器使用了全部生命周期省略规则之后,却无法计算出函数签名中引用的生命周期,编译器就会报错。

八、方法中的生命周期标注

struct上使用生命周期实现方法,语法和泛型语法一样。在哪里声明和使用生命周期参数,依赖于生命周期参数是否和字段、方法的参数或返回值有关。在struct字段的生命周期名需要在impl后声明,在struct名后使用,这些生命周期是struct类型的一部分。

impl块内的方法签名中,引用必须绑定于struct字段引用的生命周期,或者引用是独立的也可以。生命周期省略规则经常使得方法中的生命周期标注不是必须的。如下示例代码

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

结构体字段生命周期的名总是被声明在impl之后,应用于结构体名ImportantExcerpt之后,不能忽略。

level方法的返回值是i32类型,不会引用任何数据,根据第1个省略规则,可以不为&self标注生命周期。

announce_and_return_part这个方法拥有两个参数,都是引用,返回类型也是一个引用。根据第1条省略规则,就会为这两个参数添加各自的生命周期,由于其中一个参数是&self,根据第3条省略规则,它的返回的引用的生命周期就是self的生命周期。

九、静态生命周期

'static是一个特殊的生命周期,它表示整个程序的生命周期。例如:所有字符串字面值都拥有'static生命周期,如下

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

实际上,所有字符串字面值的生命周期都是'static,即整个程序的生命周期。但我们要注意的是,在为引用指定'static生命周期之前要考虑:是否需要引用在整个生命周期内都存活。因为在大部分情况下,错误的原因都在于尝试创建一个悬垂引用,或者是可用生命周期不匹配。这个时候应该尝试去解决这个问题,而不是来指定'static生命周期。

十、泛型参数类型、Trait Bound、生命周期

下面是一个同时使用了泛型类型、Trait Bound、生命周期的综合示例

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
    }
}


fn main() {
    let str1 = "abc";
    let str2 = "abcde";

    let data = "initial contents";
    let ann = data.to_string();

    let longest_str = longest_with_an_announcement(str1, str2, ann);

    println!("The longest str is {}", longest_str);
}