
1.2 生命周期
引用的生命周期受限于引用指向的变量,而引用的容器变量则不受此限制,这会导致空悬引用的问题,所以我们需要让其受限,这就是生命周期机制。
生命周期标注,应该要知道:
=> 标注的实际值来自输入参数。
=> 类型上的生命周期标注,是类型的一部分,也是一种输出。
=> 函数的返回的生命周期也是输出的一部分。
相当于是取交集。传入的多种生命周期,如果被标注为同一种,那么这种生命周期标注的生命交集。
我们知道,借用变量,实际上就是对原变量的指针变量,获取切片借用还多了一个len的参数,但实际上都要依靠原变量而存在。但是如果原变量不存在了呢?那么我们的引用会变成“野指针”,在rust中叫作垂悬引用。
如何避免这种情况发生呢?
只有“包含”关系生命周期,才允许借用。
当原变量的生命周期包含了借用变量的生命周期的时候编译器运行借用。
下面是一段借用检查器发现问题代码的示意图:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
作用域的生命周期是固定的,从作用域的第一行代码,到最后一行代码执行完毕,这就是作用域的生命周期。存活在作用域上的变量,生命周期一定跑不过作用域的生命。而依赖变量安身的数据,其寿命周期与安身的作用域无关,通常拥有无限的生命,但是对于引用数据来说生命周期上限于引用指向的变量,所以这种数据绝对不能置于生命周期高于引用生命周期的变量中。
每个借用变量都要标注生命周期,下面是个例子(你能理解所有标注的含义吗):
pub fn parse_config<'a>(args: &'a Vec<String>) -> Config<'a> {
let query: &'a String = &args[1];
let file_path: &'a String = &args[2];
Config { query, file_path }
}
pub struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
impl<'a> Config<'a> {
pub fn query(&self) -> &String {
&self.query
}
pub fn file_path(&self) -> &String {
&self.file_path
}
}
每个&引用变量,编译器都会根据所属的变量作用域进行生命周期编号,生命周期大的编号就靠前,当发现当前作用域中有&变量的编号小于当前作用域,说明这个引用的生命周期不合法。
而标注生命周期的作用是:帮助编译器判断只有运行时才能确定的引用的生命周期。通常我们需要制定一个底线来满足最差情况,将输出引用的生命周期与所有输入引用的声明周期设定为同样的生命周期,这样编译器会取传入时最小生命周期的引用的生命周期作为输出引用的生命周期。
但是在结构体中的引用生命周期,只有一个输出引用,就是结构体实例变量。编译器默认就会取所有字段引用的生命周期里最短的。我们此时不能将引用字段的生命周期设置为相同,因为当我们直接将引用传出结构体的时候,生命周期标记会跟随着引用传出,导致生命周期缩短,将不同的引用设置不同的生命周期才是正确的选择。
变量移动后,其生命周期结束了。而新的变量寿命周期开始。虽然内容一至,但是两个变量的地址早已不同。如果问原来变量的借用变量,他们仍然会去借给他的变量的地址,他并不知道原主已死。这就是变量的死亡。
.
在作用域中消亡、变量注定的命运。
结构体的生命周期
有传入有传出,记住那些标记是传入生命周期,哪些标记是传出生命周期
结构体如果内部字段有引用类型,那么我们值得对结构体的生命周期产生怀疑?是否存在结构体生命周期长于内部字段引用的生命周期?
可以使用存活时间拖累语法:
struct ImportantExcerpt<'a> {
part: &'a str,
}
这样结构体变量的存活时间一定不会超过任何一个内部字段的时间长。
如果我们的引用是通过结构体或者什么跨界行为进行传递,那么如果我们全标记同一个生命周期,那么原本长生命周期的引用会被限制,丢失了自己本身的更长的生命周期可能。
// 验证:结构体内不同生命周期标记的影响
struct Data<'a, 'b> {
forever: &'a str,
temp: &'b str,
}
pub fn test_struct_lifetime_diff() {
let f: &str = "永远活着";
let d;
let long_lived_string;
{
let t = String::from("短命");
d = Data {
forever: f,
temp: &t,
};
long_lived_string = d.forever;
} // temp 死亡
println!("{}", long_lived_string);
}
尝试将上述代码中Data的生命周期标记全为'a会导致通过结构体变量传递的引用,生命周期缩减。
编译器可以自动推断,比如只有一个输入引用的情况,以及带&self参数的方法直接绑定方法所属变量的生命。
因为这个语意,&self的方法,rust认为就应该是和&self同生命的,并且不允许我们传短命应用参数。
结构体或者方法,它们已经有命数了,它们命数就是在于它所定义变量的作用域的生命周期。然后传入的应用也是已有命数的,而生命周期标记的作用就是把这些从这些已有命数的应用进行操作,最后得到的东西,对它的命数进行一个推导。这些通过算出来的应用,它们的命数应该跟这些已有命数有关联。
静态生命周期
'static,其生命周期能够存活于整个程序期间。 所有的字符串字面值都拥有 'static 生命周期,但是在我们标注之前。
let s: &'static str = "I have a static lifetime.";
1 对输入引用进行限制
2 对静态生命周期输出引用进行生命周期标记
3 需要显示表示起提醒作用
复杂场景的生命周期标注
还有更多更复杂的涉及生命周期注解的场景,只有在非常高级的情况下才会需要它们;对于这些内容,请阅读 Rust Reference。
简单理解
同一个块中(无论impl还是fn)声明的标记都表示同一生命周期。
借用出现,那么一定要关注还引用的地方。一般来说借用变量释放即还。但是有些地方并非,借用变量又借给别人去了,导致释放借用变量的时候实际没有还完。
对于引用进行match,会导致引用生命周期与作用域延长到整个match结束才归还借用。
所以为了避免冲突,我们只能在match块中重用这个引用,而不能周期。
即使返回值中并非显式借用,但如果返回变量内部有借用,并且扯淡那么完蛋
变量本身,它有自己的生命周期和作用域。而这种类型,这个值类型,它也有自己生命的是这种,你只有两者相匹配才是合法的。
如果壳的生命周期长于纸的生命周期,那么就有可能出现空壳情况。
而如果值的生命周期长于壳的生命周期,那只能是被提前给收回,但是引用这个值,它没有任何影响,所以这是合法的。
有些借用它诞生于函数。函数具有逻辑。有些时候,编译器并不能正确地理解或者弄清楚这个引用它到底来自哪个已有的值。
所有共用的生命周期标志的引用,所有来源引用的最小集就是编译器允许的次标记的生命周期大小。
而共用其生命周期标志的输出引用,他们被允许使用的生命周期大小也就是这个最小集。
结构体的生命周期,自然要大于其内部字段的生命周期。
为了帮助我们简化简单生命周期标注,定义器默认有一些规则。
每一个输入引用有自己的生命周期标注。
如果只有一个输入引用,那么所有输出引用生命周期就都标记上它的标记。
如果有多个输入引用,但有一个是self的不可变引用或者self的可变引用,那么所有输出的引用都被标记上他的。
第三转其实就是方法返回的引用最少都要有self一样长,如果没有,咱们最好手动标记生命周期,不然编译器会拒绝编译。
首先, impl需要接一个生命周期的声明。这表明impl中的声明。
然后接结构体的全称。包括生命周期。因为生命游戏是属于这个结构体全称的一部分。
然后两个相同的声明标记将结构体本身的生命周期传递到impl块中。
impl块中的声明又可传至impl块中方法函数的同名标记中。
通过声明同名生命周期,将生命周期相连。
如果我们返回的引用标记与引用实际来源的标记不同,那么编译器会要求知道两者标记的关系。
我们可以声明一个包含关系,这先A后B,说明a大b小。
<'a: 'b, 'b>
用生命周期标记,来标记生命周期或者泛型。
就一句话,先大后小从左到右。
闭包会捕获外部变量,不像函数一样标明,这使得分析难度升级。
Rust 的临时值生命周期延长规则
在 Rust 里,一个没有名字的中间值叫 临时值(temporary),比如 foo() 的返回值、链式调用中产生的中间对象等。通常情况下,临时值会在当前语句结束时立即析构,但有些场合编译器会延长它的生命周期,让它活得比一条语句更久。
默认规则:临时值在语句结束时析构
当一个临时值被绑定到一个引用上,并且这个绑定出现在某些特定位置时,编译器会将临时值的生命周期延长至引用绑定本身的生命周期。
生命周期延长不是在所有场景下都会触发,它要求引用“直接指向”临时值的顶层
