21_rust_生命周期
生命周期
生命周期应是rust语言最与众不同的概念。
- rust的每个引用都有自己的生命周期。
- 生命周期:引用保持有效的作用域。
- 大多数情况:生命周期是隐式的、可被推断的。
- 当引用的生命周期可能以不同的方式相互关联时,需要手动标注生命周期。
生命周期存在的目的:避免悬垂引用(dangling reference)
如:
fn main() {
let r;
{
let x = 5;
r = &x; // 编译报错
}
println!("{}", r);
}
这里编译报错,因为当最后一行使用r的时候的,r所指向的x已经释放了,生命周期已结束,再继续使用r将可能访问到已释放的内存的错误。rust采用了借用检查器来检查内存问题。
借用检查器
rust编译器的借用检查器:比较作用域来判断所有的借用是否合法。
如前面例子,编译器会检查r和x的生命周期,r是x的引用,但却比x存活时间更长,编译就无法通过了。
函数中的泛型生命周期
继续从一个例子开始,利用函数longer_str返回一个更长的字符串,参数是两个字符串切片。
fn main() {
let s1 = String::from("abc");
let s2 = "bcd";
let ret = longer_str(s1.as_str(), s2);
println!("{}", ret);
}
fn longer_str(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
/*编译报错:
error[E0106]: missing lifetime specifier
--> src/main.rs:7:36
|
7 | fn longer_str(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
7 | fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
*/
编译报错:missing lifetime specifier,意思是缺少生命周期的标注,帮助信息可看出说函数的返回值包含一个借用值,但函数的签名却没有说明是借用x还是y,考虑引入一个命名的生命周期参数'a
。
看longer_str的返回值可能是x也可能是y,而x和y的生命周期在函数作用域内也无法判断,借用检查器也无法判断出返回值是的生命周期是x还是y。其实如果让函数只返回x:
fn longer_str(x: &str, y: &str) -> &str {
x
}
报错也是一样的,因为函数签名依然无法判断出x的生命周期,需要通过泛型生命周期参数来标注:
fn main() {
let s1 = String::from("abc");
let s2 = "bcd";
let ret = longer_str(s1.as_str(), s2);
println!("{}", ret);
}
// 表示x和y的生命周期与泛型标注'a的生命周期一致
fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
生命周期的标注语法
- 生命周期的标注不会改变引用的生命周期长度
- 当指定了泛型生命周期参数,函数可接收带有任何生命周期的引用
- 生命周期的标注,描述了多个引用的生命周期间的关系,但不影响生命周期
生命周期标注——语法
- 生命周期参数名:
- 以
'
号开头 - 通常全小写且非常短
- 常用
'a
标注
- 以
- 生命周期标注的位置:
- 在引用的
&
符号后 - 使用空格将标注和引用类型分开
- 在引用的
&i32 //一个引用
&'a i32 //带有显式生命周期的引用
&'a mut i32 //带有显式生命周期的可变引用
单个生命周期标注本身没有意义,因为标注的目的是提供参数变量之间的生命周期关系。比如前面的例子,x和y的生命周期必须与泛型标注'a的生命周期一致才行。
函数签名中的生命周期标注
泛型生命周期参数声明在:函数名和参数列表之间的<>
里。
fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {}
例子中所表达的意思是,函数参数x、y和返回值的生命周期都不能短于'a
。不过手动标注声明周期'a
并未改变返回值、x和y的生命周期,只是向借用检查器指出了一些可用于借用检查的非法约束,在longer_str函数里,并不需要知道x、y的存活时长,而只需知道某个作用域能够代替'a
,同时满足函数的签名约束。例子中生命周期'a
的实际生命周期是x和y两个生命周期中比较小的那个:
fn main() {
let s1 = String::from("abcd");
let ret;
{
let s2 = String::from("bcd");
ret = longer_str(s1.as_str(), s2.as_str());
}
println!("{}", ret);
}
fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
/*编译报错:
error[E0597]: `s2` does not live long enough
--> src\main.rs:6:39
|
5 | let s2 = String::from("bcd");
| -- binding `s2` declared here
6 | ret = longer_str(s1.as_str(), s2.as_str());
| ^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `s2` dropped here while still borrowed
8 | println!("{}", ret);
| --- borrow later used here
*/
上面的例子修改s2不再是字面常量,而是也是个String类型,这样使得s2的作用域仅在里边大括号内,最后编译报错如附文注释所示,意思就是报调用点s2的生命周期不够,它被调用函数内借用,但在打印点未存活,虽然看代码可看出会返回s1的借用,但编译阶段并不识别这种情况,可见的确取了更短的那个生命周期作为函数longer_str内的作用域。
深入理解生命周期
1)指定生命周期参数的方式依赖于函数所做的事情。
2)函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配。
3)如果返回的引用没有指向任何参数,那它只能引用函数内创建的值,则为悬垂引用,因为函数调用结束时,就离开了作用域。
fn main() {
let s1 = String::from("abcd");
let ret = longer_str(s1.as_str());
println!("{}", ret);
}
fn longer_str<'a>(x: &'a str) -> &'a str {
let ret = String::from("abc");
ret.as_str()
}
//编译报错:
/**
error[E0515]: cannot return reference to local variable `ret`
--> src\main.rs:8:5
|
8 | ret.as_str()
| ^^^^^^^^^^^^ returns a reference to data owned by the current function
*/
上面例子就是返回了一个内部变量的引用,并给了main函数的ret,且还在使用,但函数内部是局部变量,内存已经释放,如果想返回函数内部的值,直接返回即可,发生move所有权的操作:
fn main() {
let s1 = String::from("abcd");
let ret = longer_str(s1.as_str());
println!("{}", ret);
}
fn longer_str<'a>(x: &'a str) -> String {
let ret = String::from("abc");
ret
}
移交所有权后,内存清理由调用方清理。
生命周期的语法就是用来关联函数参数与返回值之间生命周期的,一旦获得了某种联系,rust就获得了足够的信息,来支持保证内存安全操作,避免悬垂指针及其他违反内存安全的错误。
Struct定义中的生命周期标注
Struct里可包括:
- 自持有的类型
- 引用:需要在每个引用上添加生命周期标注
struct St1<'a> { // 和函数标注一样
f1: &'a str,
}
fn main() {
let s1 = String::from("ab.cd");
let tmp = s1.split('.').next().expect("no '.' found");
let st1 = St1 {
f1: tmp, // tmp的生命周期比st1长
};
}
生命周期的省略
- 每个引用都有生命周期
- 需要为使用生命周期的函数或struct指定生命周期参数
fn test_func(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
但是上面例子是可编译通过的,其实在rust的早期版本(1.0)的时候是编译不通过的,那时候要求每个引用必须有个显式的生命周期,需要程序员手动对所有引用标注生命周期:
fn test_func<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
后来发现,rust程序员做了很多重复的生命周期标注的事情,而且这些场景是可预测的,后来rust编译团队将这些场景写入了借用检查器,使得编译器能够对类似的场景的生命周期进行自动推导,无需再标注。后期应会有更多的场景被固化,需要手动标注声明周期的地方越来越少。
在rust引用分析中所编入的模式称之为生命周期省略规则。这些规则无需开发者遵守,多为一些特殊情况,由编译器负责推断,针对符合规则的代码则无需手动标注生命周期了。不过生命周期省略规则不会提供完整的推断,若应用规则后依然无法确定,则编译报错,此时仍需手动标注。
输入、输出生命周期
- 生命周期在函数/方法的参数中:输入生命周期
- 生命周期在函数/方法的返回值中:输出生命周期
生命周期省略的三个规则
编译器使用3个规则应用于无显示标注生命周期的情况,来确定引用的生命周期:
- 规则1应用于输入生命周期
- 规则2、3应用于输出生命周期
- 如果应用3个规则后仍无法确定生命周期的引用,则编译报错
- 这些规则适用于fn定义和impl块
规则:
- 规则1:每个引用类型的参数都有自己的生命周期
- 规则2:如果只有1个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
- 规则3:如果有多个输入生命周期参数,但其中一个是&self或&mut self(是方法),那么self的生命周期会被赋给所有的输出生命周期参数
例子1:
fn func(s: &str) -> &str {} // 有这样一个函数
fn func<'a>(s: &'a str) -> &str {} // 首先应用规则1,给所有输入生命周期进行标注,每个参数有自己的生命周期
fn func<'a>(s: &'a str) -> &'a str {} // 因为只有1个参数,所以可应用规则2,给返回值标注生命周期
//至此所有输入输出参数都有了生命周期,无需应用第三条规则,编译器继续分析代码
例子2:
fn longer(x: &str, y: &str) -> &str {} // 一个两个参数的函数
// 首先应用第一条规则给所有输入参数标注,因为有两个参数,所以有两个生命周期
fn longer<a, b>(x: &:'a str, y: &:'b str) -> &str {}
// 由于有两个生命周期标注,第二条规则就不适用了。
// 而且又因longer是一个函数而不是方法,没有self参数,因此第三条规则也无法应用。
// 尝试三条规则后,发现仍然无法确定返回值的生命周期,所以编译报错。
总结:编译器尝试应用所有生命周期规则之后,仍然无法判断出签名的所有引用的生命周期时,则编译报错。
方法定义中的生命周期标注
在struct上使用生命周期实现方法,语法和泛型参数的语法一样,
在哪里声明和使用生命周期参数,依赖于:生命周期参数是否和字段、方法的参数或返回值有关。
struct字段的生命周期名:
- 在impl后声明
- 在struct名后使用
- 这些生命周期是struct类型的一部分
impl块内的方法签名中:
- 引用必须绑定于struct字段引用的生命周期,或者引用是独立的
- 生命周期省略规则经常使得方法中的生命周期标注不是必须的
struct St2<'a> {
p: &'a str,
}
// struct字段的生命周期的名字总是应用于impl关键字后及结构体名后
impl<'a> St2<'a> {
// 前面两个'a是不能忽略的,但根据省略规则,无需标注func1的生命周期
fn func1(&self) -> i32 { //不会引用任何东西
3
}
// 根据第一条省略规则会给self和a进行生命周期标注,根据第三条规则,返回值会被赋予self参数的生命周期
fn func2(&self, a: &str) -> &str { // 这样所有的生命周期都计算出来了,编译通过
println!("{}", a);
self.p
}
}
静态生命周期
static
是一个特殊的生命周期:为整个程序的持续时间。
比如,所有字符串字面值都拥有"'static"生命周期:
let s: &'static str = "test";
它是被存储在二进制程序中的,所以总是可用的,因此字符串的生命周期都是'static
。
泛型参数类型、Trait Bound、生命周期的例子
use std::fmt::Display;
fn longer<'a, T> (x: &'a str, y: &'a str, z: T) -> &'a str
where
T: Display,
{
println!("{}", z);
if x.len() > y.len() {
x
} else {
y
}
}