生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据。当存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。生命周期标注并不会改变任何引用的实际作用域,自身并不具有什么意义,其作用只是告诉编译器多个引用之间的关系。生命周期的标注以 '
开头,名称往往是一个单独的小写字母,大多数人都用 'a
来作为生命周期的名称。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
注意的点如下:
- 和泛型一样,使用生命周期参数,需要先声明
<'a>
x
、y
和返回值至少活得和'a
一样久(因为返回值要么是x
,要么是y
)
该函数签名表明对于某些生命周期 'a
,函数的两个参数都至少跟 'a
活得一样久,同时函数的返回引用也至少跟 'a
活得一样久。即表示返回值的生命周期与参数生命周期中的较小值一致 。当把具体的引用传给 longest
时,那生命周期 'a
的大小就是 x
和 y
的作用域的重合部分。
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
在上述代码中,result
必须要活到 println!
处,因为 result
的生命周期是 'a
,因此 'a
必须持续到 println!
。在 longest
函数中,string2
的生命周期也是 'a
,由此说明 string2
也必须活到 println!
处,可是 string2
在代码中实际上只能活到内部语句块的花括号处 }
,小于它应该具备的生命周期 'a
,因此编译出错。
函数的返回值如果是一个引用类型,那么它的生命周期只会来源于:(1)函数参数的生命周期
(2)函数体中某个新建引用的生命周期,此时就会引起悬垂引用,其好的解决办法就是返回内部字符串的所有权,然后把字符串的所有权转移给调用者。
在结构体中也可以使用引用,只要为结构体中的每一个引用标注上生命周期即可,需要对生命周期参数进行声明 <'a>
。该生命周期标注说明,结构体 ImportantExcerpt
所引用的字符串 str
必须比该结构体活得更久。
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let i;
{
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
i = ImportantExcerpt {
part: first_sentence,
};
}
println!("{:?}",i);
}
上面main函数中,可以看出结构体比它引用的字符串活得更久,引用字符串在内部语句块末尾 }
被释放后,println!
依然在外面使用了该结构体,因此会导致无效的引用,编译报错。
函数或者方法中,参数的生命周期被称为 输入生命周期
,返回值的生命周期被称为 输出生命周期。
编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期。若编译器发现三条规则都不适用时,就会报错,需要手动标注生命周期。
-
每一个引用参数都会获得独自的生命周期
例如,两个引用参数的有两个生命周期标注:
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
-
若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期
例如函数
fn foo(x: &i32) -> &i32
,等同于fn foo<'a>(x: &'a i32) -> &'a i32
-
若存在多个输入生命周期,且其中一个是
&self
或&mut self
,则&self
的生命周期被赋给所有的输出生命周期
在 impl
内部的方法中,如果没有用到生命周期 'a
,那就可以写成 '_
来表示有一个不使用的生命周期,我们可以忽略它,无需为它创建一个名称。
impl Reader for BufReader<'_> {
// methods go here
}
并不能由于没有用到生命周期而不写,因为生命周期参数也是类型的一部分。
为具有生命周期的结构体实现方法时,其语法跟泛型参数语法很相似:
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
impl
中必须使用结构体的完整名称,包括<'a>
,因为生命周期标注也是结构体类型的一部分!- 方法签名中,往往不需要标注生命周期,得益于生命周期消除的第一和第三规则
如果要表示两个生命周期长短,可用如下约束语法:
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
'a: 'b
,是生命周期约束语法,跟泛型约束非常相似,用于说明'a
必须比'b
活得久- 可以把
'a
和'b
都在同一个地方声明(如上),或者分开声明但通过where 'a: 'b
约束生命周期关系
T:'a表示类型 T
必须比 'a
活得要久。
struct Ref<'a, T: 'a> {
r: &'a T
}
因为结构体字段 r
引用了 T
,因此 r
的生命周期 'a
必须要比 T
的生命周期更短(被引用者的生命周期必须要比引用长)。
静态生命周期'static
可以帮助我们解决非常复杂的生命周期问题甚至是无法被手动解决的生命周期问题。它有两种用法: &'static
和 T: 'static;
实在遇到解决不了的生命周期标注问题,可以尝试 T: 'static。
生命周期 'static
意味着能和程序活得一样久,例如字符串字面量和特征对象。
和程序活得一样久指的是引用指向的数据,而非引用,而引用本身是要遵循其作用域范围的。
fn main() {
{
let static_string = "I'm in read-only memory";
println!("static_string: {}", static_string);
// 当 `static_string` 超出作用域时,该引用不能再被使用,但是数据依然会存在于 binary 所占用的内存中
}
println!("static_string reference remains alive: {}", static_string);
}
原因在于虽然字符串字面量 "I'm in read-only memory" 的生命周期是 'static
,但是持有它的引用并不是,它的作用域在内部花括号 }
处就结束了。
&'static
生命周期针对的仅仅是引用,而不是持有该引用的变量,对于变量来说,还是要遵循相应的作用域规则 :
use std::{slice::from_raw_parts, str::from_utf8_unchecked};
fn get_memory_location() -> (usize, usize) {
// “Hello World” 是字符串字面量,因此它的生命周期是 `'static`.
// 但持有它的变量 `string` 的生命周期就不一样了,它完全取决于变量作用域,对于该例子来说,也就是当前的函数范围
let string = "Hello World!";
let pointer = string.as_ptr() as usize;
let length = string.len();
(pointer, length)
// `string` 在这里被 drop 释放
// 虽然变量被释放,无法再被访问,但是数据依然还会继续存活
}
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
// 使用裸指针需要 `unsafe{}` 语句块
unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}
fn main() {
let (pointer, length) = get_memory_location();
let message = get_str_at_location(pointer, length);
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer, message
);
// 处理裸指针需要 `unsafe`,例如:
// let message = get_str_at_location(1000, 10);
}
&'static
的引用确实可以和程序活得一样久,因为我们通过get_str_at_location
函数直接取到了对应的字符串
T: 'static
与 &'static
有相同的约束:T
必须活得和程序一样久。
use std::fmt::Display;
fn main() {
let r1;
let r2;
{
static STATIC_EXAMPLE: i32 = 42;
r1 = &STATIC_EXAMPLE;
let x = "&'static str";
r2 = x;
// r1 和 r2 持有的数据都是 'static 的,因此在花括号结束后,并不会被释放
}
println!("&'static i32: {}", r1); // -> 42
println!("&'static str: {}", r2); // -> &'static str
let r3: &str;
{
let s1 = "String".to_string();
// s1 虽然没有 'static 生命周期,但是它依然可以满足 T: 'static 的约束
// 充分说明这个约束是多么的弱。。
static_bound(&s1);
// s1 是 String 类型,没有 'static 的生命周期,因此下面代码会报错
r3 = &s1;
// s1 在这里被 drop
}
println!("{}", r3);
}
fn static_bound<T: Display + 'static>(t: &T) {
println!("{}", t);
}