rust踩雷笔记3——生命周期的理解

目录

生命周期是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的生命周期在花括号内,上述代码没有悬垂引用。

那你觉得此时,编译器报不报错?

当然会!记住两个原则:

  1. 生命周期标注不改变引用的真实生命周期(比如上述的y还是在花括号内)
  2. 编译器会相信你标注的生命周期,而不是引用真实的生命周期

没错,当你标注'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,但是编译器发现和事实不符,于是报错。

⚠️总结:

  1. 你给编译器说什么,编译器就信什么,所以你不能乱说,要标注正确的生命周期,以及不同生命周期的关系。如果编译器从你的标注就感觉到了悬垂引用,就会报错;
  2. 你标注了什么,不代表真实的生命周期就是什么,你的标注只是告诉编译器你希望的事实,如果实际情况不满足标注的约束,编译器就会报错。

简版:

你的标注若有悬垂引用,哪怕实际上没有,编译器也会报错;

你的标注若与事实不符,标注没有悬垂引用而实际有或者有风险发生,编译器报错。

所以是否报错要看:

标注是否正确+事实是否符合正确的标注约束

相关推荐
hccee16 分钟前
C# IO文件操作
开发语言·c#
hummhumm21 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊31 分钟前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
zmd-zk1 小时前
flink学习(2)——wordcount案例
大数据·开发语言·学习·flink
好奇的菜鸟1 小时前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.01 小时前
Go语言进阶&依赖管理
开发语言·后端·golang
花海少爷1 小时前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
手握风云-1 小时前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
喵叔哟1 小时前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生1 小时前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea