目录
生命周期是rust中最难的概念------鲁迅
这一块内容即便是看rust圣经,第一遍也有点懵。今天早上二刷突然有了更直观的认识,记录一下。
概念和基本使用
生命周期就是应用的有效作用域,它的主要作用是避免悬垂引用。
悬垂引用的典型例子:
rust
{
let y;
{
let x = 1;
y = &x;
}
println!("y: {}", y);
}
简而言之就是y引用的变量在y被使用之前就释放了。
我们通过肉眼检查上面代码,会发现x的生命周期没有延续到y被使用的时候,所以会发现问题。rust为了安全做出了很多约束,所以这里即便是我们不用肉眼观察,也能发现问题。
一个例子彻底理解最基本的内容
让我们直接切入使用,来体会生命周期的用法。
首先,就算你什么都不做,编译器大多数可以自动推导出生命周期。
如果你要手动标注声明周期,那么就加'a
符号,'
是重点,a的话可以替换。
看如下代码:
rust
let y;
{
let x = 1;
y = &x;
}
println!("y: {}", y);
很显然发生了悬垂引用,报错信息:
x does not live long enough, borrowed value does not live long enough
x的生命周期在花括号内,想要避免悬垂引用,需要将x的生命周期延长到y的位置
方法一✔️:
rust
let x = 1;
let y;
{
y = &x;
}
println!("y: {}", y);
这样肯定就没错了,x作为被引用的变量,生命周期比引用它的y长
方法二❎:
不改变代码,只是添加生命周期引用?
当然不行!
生命周期引用只是为了向编译器做出说明,如果加了
'a
的话,就说明引用的作用域大于等于'a。如果对多个引用加同一个'a,说明这些引用的作用域都大于等于'a。可以看到,这是一种约束条件;生命周期标注'a并不改变引用的真实生命周期,只是告诉编译器,当不满足'a表示的约束条件时,就报错。
⭐️一个例子体会生命周期的作用------让编译器检查约束条件
rust
let x = 5;
{
let y = &x;
}
这个代码肯定没有问题,被引用的x比引用y活得久。
那如果此时我加入生命周期约束
rust
let x = 5;
{
let y: &'static i32 = &x;
}
这是在告诉编译器y具有全局生命周期,但实际上y的生命周期在花括号内,上述代码没有悬垂引用。
那你觉得此时,编译器报不报错?
当然会!记住两个原则:
- 生命周期标注不改变引用的真实生命周期(比如上述的y还是在花括号内)
- 编译器会相信你标注的生命周期,而不是引用真实的生命周期
没错,当你标注'static起,编译器就认为y可以活全局,那么此时它检查x和y的约束关系,就会按照这个来。所以报错信息是:
x does not live long enough, borrowed value does not live long enough
编译器觉得x不能活全局,所以y活全局的话,会发生悬垂引用。
⚠️注意:
问:上述代码有没有发生悬垂引用?
答:没有。
问:'static有没有改变y的真实生命周期?
答:没有。
问:那为啥报了悬垂引用的错误?
答:因为编译器相信你的标注,从而它认为y就是活全局的。
一个例子理解函数签名为什么要有生命周期标注
摘自rust圣经的一段代码:
rust
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
这个程序会报错:
help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from x or y
我们来分析一下,当longest被调用的时候,x引用string1,y引用string2(严格讲string2本身是&str)。result因为获取返回值,所以是引用string1或者string2,那么问题来了,rust因为严格的安全检查,此时要检查是否有悬垂引用,即检查是否有被引用的变量生命周期大于引用变量。
可是编译期间是无法获知result会引用哪个字符串的,编译器这个大聪明就无法检查是否有悬垂引用。
rust圣经说了,改成这个就不会有问题了:
rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
记住分析:编译器会认为x和y生命周期都不小于'a,并且'a的大小就是x和y中生命周期的交集。所以相当于告诉了编译器,把具体引用传给返回值的时候,返回值的生命周期为x和y生命周期的交集。
如下例子深入理解:
(依旧摘自rust圣经)
rust
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);
}
分析:我们告诉编译器的结论是,'a的生命周期是string1和string2的交集,也就是string2的生命周期;因此result生命周期等于string2的生命周期。
然后,编译器根据我们的结论,去检查代码有没有悬垂引用的风险,虽然上述函数调用,会使得result引用的是string1,这样一看没有悬垂引用。但是编译期间不知道result会引用谁,此时编译器一看,如果引用的是string2,那么悬垂引用就会发生,所以就会报错。
由于此例子中result只会引用string1,所以代码也许可以这样改:
rust
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
就不会出错了。
先写这么多,剩下的结合实践深入了解。
我又回来了,因为发现一个更好做说明的例子:
⭐️能不能对编译器蒙混过关?
这个标题什么意思呢,我们再把rust圣经中一段代码摘过来
rust
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);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
刚刚说了,这个生命周期标注会让编译器意识到有悬垂引用风险,于是拒绝代码执行
safety这个单词被发明出来前,还有个单词表示安全,叫rusty------鲁迅
这时候咱可能不爽了,你看明明string1更长,longest只会返回对string1的引用,肉眼就能发现没有悬垂引用,为什么编译器这个大聪明一定要去检查string2呢?
不行,不能惯着编译器,我们修改代码:
rust
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
我直接把y的'a标注删掉,看你怎么检查string2.
结果编译器也是倔强,反手来了个新错误:
explicit lifetime required in the type of y, lifetime 'a required
也就是说没有标注编译器就没办法检查悬垂引用,没办法检查就会直接报错。
那我继续改:
rust
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
改成'b总可以了吧,还是不行:
lifetime may not live long enough, consider adding the following bound: 'b: 'a
'b: 'a
表示'b生命周期大于'a。
那继续改:
rust
fn longest<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
结果依旧是报错:
string2 does not live long enough
。
你会发现,
- 不标注y生命周期的时候,编译器因为无法检查悬垂引用而报错;
- 给y标注一个'b后,因为函数返回值是'a,编译器无法判断'b和'a的关系,也就无法检查悬垂引用;
- 标注'a: 'b或者'b: 'a都可以表示两者关系,但是我们只能选没有悬垂引用的那种;
- longest返回值是一个引用,它可能会引用y,所以这里只能是'b: 'a,如果你标注'a: 'b,说明你告诉编译器返回值获得比y久,那么编译器会相信你说的,就直接因为可能发生悬垂引用报错;
- 那你正好标注'b: 'a,但是编译器发现和事实不符,于是报错。
⚠️总结:
- 你给编译器说什么,编译器就信什么,所以你不能乱说,要标注正确的生命周期,以及不同生命周期的关系。如果编译器从你的标注就感觉到了悬垂引用,就会报错;
- 你标注了什么,不代表真实的生命周期就是什么,你的标注只是告诉编译器你希望的事实,如果实际情况不满足标注的约束,编译器就会报错。
简版:
你的标注若有悬垂引用,哪怕实际上没有,编译器也会报错;
你的标注若与事实不符,标注没有悬垂引用而实际有或者有风险发生,编译器报错。
所以是否报错要看:
标注是否正确+事实是否符合正确的标注约束