5分钟速读之Rust权威指南(十九)

499 阅读7分钟

生命周期

前面章节中讲到引用与借用的时候,里边有一个细节没有讲到,就是生命周期的概念,rust的每个引用都有自己的生命周期,生命周期最主要的目标在于避免悬垂引用:

认识生命周期

变量会在自身所在的生命周期内有效,当作用域结束后,会将其中的变量销毁,变量的引用也将不再可用:

{
  let r;

  { // 开始一个新的作用域
    let x = 5; // x被创建了
    r = &x; // 报错,x的生命周期不够长
  } // 因为x在这里销毁

  println!("r: {}", r);
}
// 因为r是x的引用,而x销毁的时候,x的引用也自然不可用了
// 所有r也会变成悬垂引用,这在编译过程是不通过的

rust并不是任何时候都可以识别生命周期,比如将引用作为函数返回值时,编译器不能识别返回的引用是来自于哪个参数:

fn longest(x: &str, y: &str) -> &str { // 报错,返回值需要指定生命周期
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

有同学可能觉得是因为有判断逻辑的原因,所以编译器不确定会返回哪个值,下面可以去掉判断试一下:

fn longest(x: &str, y: &str) -> &str { // 报错,返回值需要指定生命周期
  x
}

还是报错了,说明错误原因的不在于函数体,而是在于开发者的传参,参数需要与返回值产生联系,如果没有联系,那么rust将无法在编译阶段得知函数执行后的返回值会在什么时候销毁,标注生命周期以后,就可以限制函数的调用者的使用方式。


生命周期注解语法

生命周期参数名称必须以单引号(')开头,其名称通常全是小写,类似于泛型,其名称非常短。'a 是惯用使用的名称。生命周期参数注解位于引用符号&之后,并有一个空格来将引用类型与生命周期注解分隔开:

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

下面来尝试对longest函数进行标注生命周期:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  // 对x和y都标记了生命周期为'a,返回值的生命周期也是'a
  // 意味着longest的返回值的生命周期截止是'a的销毁时间,
  // 而'a代表了运行时x、y中较短的那个。
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

// 可以正常编译通过了
println!("{}", longest("abc", "a")); // abc

通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用:

let string1 = String::from("abcd");

{
  let string2 = String::from("xyz");
  let result = longest(string1.as_str(), string2.as_str());
  println!("最长的字符串是:{}", result); // 最长的字符串是:abcd
}
// 上边由于传参符合函数对于生命周期的要求,
// string2是x、y参数中生命周期最短的那个,
// 根据函数签名推断出,返回值result与string2的生命周期的长度都是'a,
// 而在作用于结束后,外部没有对string2悬垂引用,所以编译通过了

知道了result的生命周期和string2相同,也就能推断出悬垂引用:

fn main() {
  let string1 = String::from("abcd");
  let result;
  {
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str()); // 报错, string2.as_str()的生命周期不够长
  }
  println!("最长的字符串是:{}", result);
}
// 再重复一下,
// 由于我们对函数参数和返回值的生命周期标注为x、y中较短的那一个,
// 上面string2的生命周期最短,也就是说result和string2的生命周期是相同的
// 当string2所在的作用域被销毁后
// result仍被外部作用域引用,也就产生了悬垂引用,所以报错了。

如果都函数签名中生命周期没有争议,也可以编译通过:

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
  x
}
// 因为编译器能够很明确知道返回值的引用来自于x,
// 因为y没有被标注生命周期。

那如果返回值是函数体中创建的引用呢:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 对生命周期做了标记
	let s = String::new(); // 返回值是函数体中创建的引用
	&s // 报错,不能返回对局部变量s的引用,返回值是对当前函数的引用
}
// s在函数执行完成后被销毁,&s变成了悬垂引用,
// 整个过程与参数、返回值的生命周期标注并没有发生关联,所以报错。

结构体定义中的生命周期注解

如果结构体中的字段是引用类型,那么同样需要标注生命周期:

struct ImportantExcerpt<'a> { // 在结构体名称后面声明
  part: &'a str, // 表示part的生命周期至少要比结构体长,否则就产生的悬垂引用
}

fn main() {
  let novel = String::from("Call me Ishmael. Some years ago...");
  // 这里只是将字符串拆分成了段,获取了第一段
  let first_sentence = novel.split('.')
    .next()
    .expect("Could not find a '.'");
  let i = ImportantExcerpt { part: first_sentence };
  // 这里first_sentence和i都在main函数结束后被销毁
  // first_sentence早于i创建,所以能够正常编译通过
}

生命周期省略(Lifetime Elision)

生命周期都符合以下三条规则,编译器利用这三条规则可以让开发者省略部分生命周期的标注,提高开发效率:

  • 每一个是引用的参数都有它自己的生命周期参数。
  • 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
  • 第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,那么所有输出生命周期参数被赋予 self 的生命周期。

看一个可以省略生命周期的例子:

// 符合规则一和二
fn first_word(s: &str) -> &str {}
// 编译器自动生成
fn first_word<'a>(s: &'a str) -> &'a str {}

这个例子则需要开发者手动标注:

fn longest(x: &str, y: &str) -> &str {}
// 不符合规则二和三,需要开发者手动标注

为包含引用类型字段的结构体,为其实现方法的时候,也需要标注生命周期:

impl<'a> ImportantExcerpt<'a> {
  // impl 之后和类型名称之后的生命周期参数是必要的,
  // 不过因为第一条生命周期规则我们并不必须标注self引用的生命周期。
  fn level(&self) -> i32 {
    1
  }
}

下面announce_and_return_part函数参数中包含self引用,结合第三条规则,生命周期是可以省略的:

impl<'a> ImportantExcerpt<'a> {
  fn announce_and_return_part(&self, announcement: &str) -> &str {
    self.part
  }
}

静态生命周期

静态生命周期能够存活于整个程序期间。所有的字面值都拥有 'static 生命周期:

let s = "I have a static lifetime.";
// 等同于
let s: &'static str = "I have a static lifetime.";

作为函数返回值:

// 上面的标注方式
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  &"abc"
}

// 或者直接标识返回值是静态的
fn longest<'a>(x: &str, y: &str) -> &'static str {
  &"abc"
}

编译通过了,是因为字面量的字符串"abc"是被rust硬编码到编译结果中,这是不会被销毁的,事实上所有的字面量都是可以的:

fn longest<'a>(x: &'a str, y: &'a str) -> (&'a str, &'a i32) {
  (&"abc", &123)
}
println!("{:?}", longest("a", "b"));// ("abc", 123)
// 上边返回一个元组,包含两个字面量,编译是正常的。

同时使用泛型参数、trait约束与生命周期

因为生命周期也是泛型的一种,所以生命周期参数'a和泛型参数T都被放置到了函数名后的尖括号列表中:

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

longest_with_an_announcement("a", "b", 1); // Announcement! 1

生命周期理解起来还是不容易的,所以下一节打算再深度剖析一下为什么要设计生命周期