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 有个原则,当引用类型出现在另一个类型的定义中时,必须为引用标注生命期。上面的代码有两中解决办法:所有的引用都标注静态生命期,或者指定一个自定义的生命期。
- 所有的引用都标注静态生命期(不推荐)
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
,保存在 name
,age
中任何引用的值都包含生命期 a
,生命期a
也必须比保存 Student
的值长。
0x02 哪些值具有 'static 生命期
继续观察上面的代码,当我使用 &'static i32'
时,传入了一个 const
值的引用,而name
却使用的let
,这里的const
可以改成 let
吗?答案是不行。
要搞清楚这个问题,首先要明白在 Rust 中哪些值的生命期是 'static'
,上一篇文章我只是简单说了下字符串字面量是静态生命期,那还有哪些值具有静态生命期呢?下面先看一张图。
这是一张内存简易图,拥有静态生命期的区域就是紫色区域了。
- BSS Segment:存放全局变量和静态变量的一块内存区域,可读写。
- Data Segment:存存放常量、字符串字面量等数据的区域,只读。
- Text Segment:存放代码片段的区域,只读。
这三个区域的生命期与程序的生命期一致,所以在这些区域保存的值拥有静态生命期,他们都有个特点就是编译前这些值就是确定的。分配在堆和栈上的值的生命期是动态的。另外,如果在堆上使用了Box::leak
,那么也具有静态生命期了,使用 Box::leak
就可以将一个运行期的值转为 'static
(暂时了解即可)。
0x03 生命期省略规则
从我们开始学习到现在可以发现很少需要我们主动标明生命期。虽然没有标注,但是其实生命期是存在的。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 小结
生命期的标注仅仅是为了告知编译器,而在我们平时的大部分场景下基本都不需要标注生命期,一定要牢记省略生命期的三条规则。另外,当引用类型出现在另一个类型的定义中时,必须为引用标注生命期。