Rust 中的生命周期
生命周期的定义及意义
生命周期:引用保持有效的作用域
rust 中每个引用都有自己的生命周期,大多数情况下,生命周期是隐式的。当引用的生命周期可能以不同的方式互相关联时,则需要手动标注其生命周期
生命周期的存在主要是为了解决
悬垂引用
的问题,也就是引用变量还在,但是它所引用的值已经不存在了的情况
悬垂引用出现的情况
下面的代码中,变量 a 是变量 b 的引用,但是当变量 b 结束作用域后,程序又访问了变量 a,此时 a 所引用的值其实已经不存在了,因此这里 a 就变成了无效引用,也就是发生了悬垂引用。此时程序无法通过编译。
fn main() {
let a;
{
let b = 123;
a = &b;
}
println!("a = {}", a);
}
将上述代码中变量 b 的定义改变一下位置,即可通过编译,修改为如下:
fn main() {
let a;
let b = 123;
{
a = &b;
}
println!("a = {}", a);
}
/* 输出:
a = 123
*/
生命周期可以解决的问题举例
看下面一段代码,代码中定义了一个函数test_01
,它接收一个i32的引用类型的参数,然后再返回这个参数本身,代码可以别正常编译执行(下属代码可以被编译其实是因为编译器自动进行了生命周期推断,因此它等同于:fn test_01<'a>(a: &'a i32) -> &'a i32 { a }
)
fn main() {
let a = 123;
let b = 456;
let c = test_01(&a);
let d = test_01(&b);
println!("a = {}, b = {}, c = {}, d = {}", a, b, c, d)
}
fn test_01(a: &i32) -> &i32{
a
}
/* 输出:
a = 123, b = 456, c = 123, d = 456
*/
将上述代码进行一个小的调整,将函数test_01()
新增一个参数,类型也是i32的引用,然后返回这两个参数中较大的参数,此时会发现程序无法被编译,如下:
fn main() {
let a = 123;
let b = 456;
let large = test_01(&a, &b);
println!("c = {}", large);
}
// 下面这个函数的定义无法被编译
fn test_01(a: &i32, b: &i32) -> &i32{
if a > b {
a
} else {
b
}
}
此时编译程序会提示函数test_01()
的返回值是一个引用类型,但是无法确定函数签名中a和b的生命周期。换句话说,就是这个函数返回了变量a和b其中的一个引用给一个新的变量large
但其实编译器并不能确定它在返回这个引用后,变量a和b是否还存在。也就是说存在一种可能,函数返回了一个参数的引用给一个变量large
,但随后在变量large
尚且存在的时候,它所引用的那个变量a或b已经不存在了,此时也会发生悬垂引用
的问题
要解决上述的问题,就需要对参数a和b以及返回值的生命周期进行手动标注,如下:
需要注意的是,生命周期的标注并不会改变引用的生命周期长度,只是用来描述多个引用的生命周期之间的关系。
fn main() {
let a = 123;
let b = 456;
let large = test_01(&a, &b);
println!("c = {}", large);
}
// 通过手动将返回值的生命周期标注为和参数保持一致
// 该返回值实际的生命周期就是参数 a 和参数 b 中生命周期较小的那一个
fn test_01<'a >(a: &'a i32, b: &'a i32) -> &'a i32{
if a > b {
a
} else {
b
}
}
/* 输出:
c = 456
*/
上述代码通过'a
来表示一个生命周期,同时将两个参数和返回值的类型都标注为'a
,表示返回的这个引用和参数的作用域是相同的,此时便可以通过编译。
生命周期的定义
生命周期的参数名以单引号
'
开头,通常是全小写且简短,例如'a
。标注的位置通常放于引用符号
&
后和类型前,中间用一个空格隔开,例如:&'a i32
。从函数返回一个引用时,这个引用的生命周期必须和函数参数中的至少一个参数的生命周期所匹配
生命周期的省略规则
满足下列规则的写法可以省略对生命周期的手动标注,介绍生命周期的省略规则前先介绍两个概念:
输入生命周期:函数/方法的参数
输出生命周期:函数/方法的返回值
每个引用类型的参数都有自己的生命周期
如果只有一个输入生命周期参数,那么该生命周期被赋给所有输出生命周期参数
如果有多个输入生命周期参数,但其中一个是
&self
或&mut self
,那么self的生命周期就会被赋给所有的输出的生命周期参数
方法定义中的生命周期标注
fn main() {
let p = People {
name: &String::from("Zhangsan"),
};
let s1 = String::from("welcome to ");
let s2 = String::from("rust!");
p.say(&s1, &s2);
}
struct People<'a> {
name: &'a String,
}
impl<'a> People<'a> {
// 由于该方法的第一个参数的类型是 &self, 满足生命周期的省略规则, 因此返回值的生命周期就是该方法的返回值的生命周期
fn say(&self, s: &String, s2: &String) -> &String {
println!("Hello, {}, {}!", self.name, s);
self.name
}
}
/* 输出:
Hello, Zhangsan, welcome to rust!!
*/
静态生命周期
'static
是一个特殊的生命周期,它存在于整个程序的运行时间,常见的如&str的生命周期就是:'static
fn main() {
let a = "hello";
let b = "world";
let c = test(a, b);
println!("{}", c);
}
fn test(s: &'static str, s2: &'static str) -> &'static str {
println!("{} {}", s, s2);
s
}
/* 输出:
hello world
hello
*/
评论区