likes
comments
collection
share

【Rust 中级教程】 第17课 引用的 lifetime(2)本篇文章将继续介绍 Rust 的 lifetime 在结

作者站长头像
站长
· 阅读数 32

0x00 开篇

本篇文章将继续介绍 Rust 的 lifetime 在结构体中的使用以及 lifetime 的省略规则。本篇文章的阅读时间大约 5 分钟

0x01 结构体中的引用

先来看一个示例:

fn main() {
    let name = "zhangsan";
    let age = 18;

    let s = Student { name: name, age: &age };
    println!("{:?}", s);
}

#[derive(Debug)]
struct Student {
    name: &str,
    age: &i32,
}

上面的代码乍一看是没有问题的,但是当编译时会提示错误,来看下错误信息。

【Rust 中级教程】 第17课 引用的 lifetime(2)本篇文章将继续介绍 Rust 的 lifetime 在结

错误提示告诉我们,"缺少生命期标识符"。Rust 有个原则,当引用类型出现在另一个类型的定义中时,必须为引用标注生命期。上面的代码有两中解决办法:所有的引用都标注静态生命期,或者指定一个自定义的生命期。

  • 所有的引用都标注静态生命期(不推荐)
fn main() {
    let name = "zhangsan";
    const age: i32 = 30;

    let s = Student { name: name, age: &age };
    println!("{:?}", s);
}

#[derive(Debug)]
struct Student {
    name: &'static str,
    age: &'static i32,
}

这样改虽然能解决问题,但是我们并不推荐。这样会导致所有的值的生命期都跟整个程序一样了。

  • 指定一个自定义的生命期

我们可以指定一个生命期a,让每个结构体都拥有一个生命期 a。跟类似声明泛型一样,在结构体的后面添加 <'a>,为每个字段标注生命期。

fn main() {
    let name = "zhangsan";
    let age: i32 = 18;

    let s = Student { name: name, age: &age };
    println!("{:?}", s);
}

#[derive(Debug)]
struct Student<'a> {
    name: &'a str,
    age: &'a i32,
}

现在的每个 Student 类型都拥有一个生命期 a,保存在 nameage 中任何引用的值都包含生命期 a,生命期a 也必须比保存 Student 的值长。

0x02 哪些值具有 'static 生命期

继续观察上面的代码,当我使用 &'static i32' 时,传入了一个 const 值的引用,而name却使用的let,这里的const可以改成 let 吗?答案是不行。

要搞清楚这个问题,首先要明白在 Rust 中哪些值的生命期是 'static',上一篇文章我只是简单说了下字符串字面量是静态生命期,那还有哪些值具有静态生命期呢?下面先看一张图。

【Rust 中级教程】 第17课 引用的 lifetime(2)本篇文章将继续介绍 Rust 的 lifetime 在结

这是一张内存简易图,拥有静态生命期的区域就是紫色区域了。

  • BSS Segment:存放全局变量和静态变量的一块内存区域,可读写。
  • Data Segment:存存放常量、字符串字面量等数据的区域,只读
  • Text Segment:存放代码片段的区域,只读

这三个区域的生命期与程序的生命期一致,所以在这些区域保存的值拥有静态生命期,他们都有个特点就是编译前这些值就是确定的。分配在堆和栈上的值的生命期是动态的。另外,如果在堆上使用了Box::leak,那么也具有静态生命期了,使用 Box::leak 就可以将一个运行期的值转为 'static (暂时了解即可)。

0x03 生命期省略规则【Rust 中级教程】 第17课 引用的 lifetime(2)本篇文章将继续介绍 Rust 的 lifetime 在结

从我们开始学习到现在可以发现很少需要我们主动标明生命期。虽然没有标注,但是其实生命期是存在的。Rust 会在生命期参数合理时省略他们。三条省略的规则如下:

  • 如果一个函数的返回值不返回任何引用,那么永远都不需要标注参数的生命期。
  fn main() {
      let val1 = 5;
      let val2 = 6;
      let x = fun1(&val1, &val2);
      println!("fun1 = {}", x);
  }
  
  // 无需标注生命期
  fn fun1(a: &i32, b: &i32) -> i32 {
      return a + b + 5;
  }
  
  // 运行结果
  // fun1 = 16
  • 如果一个函数的参数只出现了一个生命期且返回值是引用,那么Rust则推断返回值的生命期与函数参数的生命期相同,也不需要标注生命期。
  fn main() {
  	let array = [6, 3];
      let x = fun2(&array);
      println!("fun2 = {:?}", x);
  }
  
  fn fun2(a: &[i32; 2]) -> (&i32, &i32) {
      return (&a[0], &a[1]);
  }
  
  // 运行结果
  // fun2 = (6, 3)
  
  • 如果函数是某个类型的方法,方法本身会接收引用形式的 self 参数,那么 Rust 将会把返回值的生命期推断为 self 的生命期。
  fn main() {
  	let a = String::from("aaa");
      let b = String::from("bbb");
      let c = String::from("ccc");
      let example = Example { data: vec![a, b, c] };
      let x = example.get_element("b");
      println!("fun3 = {:?}", x);
  }
  
  struct Example {
      data: Vec<String>,
  }
  
  impl Example {
      /// 通过前缀查找字符串
      fn get_element(&self, prefix: &str) -> Option<&String> {
          for i in 0..self.data.len() {
              if self.data[i].starts_with(prefix) {
                  return Some(&self.data[i]);
              }
          }
          None
      }
  }

在这个例子中,完整的函数签名应该是 fn get_element<'a,'b>(&'a self, prefix: &'b str) -> Option<&'a String> , Rust 会假定你无论借用什么,都是用 self 中借用的。

PS:如果一个函数存在返回值但是没有函数入参,那么返回值的生命期将会推断为 'static。不满足上面两条规则的将必须添加生命期。比如我们上一篇文章讲的示例 longest(x: &str, y: &str) -> &str

0x04 小结

生命期的标注仅仅是为了告知编译器,而在我们平时的大部分场景下基本都不需要标注生命期,一定要牢记省略生命期的三条规则。另外,当引用类型出现在另一个类型的定义中时,必须为引用标注生命期

转载自:https://juejin.cn/post/7224335234010005559
评论
请登录