1 生命周期: 连接时空的桥梁对我来说, 生命周期很神奇.在一段程序中, 时间是变量的存活时间, 而空间是变量指向的物理内存区域.写过操作系统的人都知道, 内存地址其实甚至不由 C 语言来管理, 它们由操作系统的内核管理.也就是说, 实际上访问了一块错误内存, 会立马触发 #PF, 然后内核的 page_fault_handler 发送 SIGSEGV 信号给程序, 程序执行默认的 SIGSEGV Handler.至于时间, 那 C 语言更是管不了了, push_ref 之类的操作全凭程序员自觉.但是, Rust 却有意识的将时间和空间维度给结合起来, 创建了一套新的时间规则, 来约束空间规则, 也就是生命周期.这也是我感觉精妙的点.2 生命周期的意义一个 Rust 萌新可能会对这样的代码产生生理性的厌恶:fn foo(a: str, b: str) - str {b}这段代码可以说必然会被 Rust 斩首, 因为它未明确生命周期.正确的代码应该是这样的:fn fooa(a: a str, b: a str) - a str {b}这类型的问题其实我们已经司空见惯. 但是为什么第一个为什么就会被斩首, 而第二个就行呢? 生命周期到底是什么?2.1 生命周期的标记在我看来, Rust 的生命周期其实是一段范围. 但是这个范围我们目前不知道, 由编译器确定. 所以我们设一个未知数, 例如 x, 来表示这个生命周期的范围.我也不解释太多, 更多的解释详见:The Rustonomicon Section 3.3 - Lifetimes相信我, 如果你学过 The Book 的话, 你肯定看得懂, 并且会 “哦”. 它会教你如何对一段 Rust 程序进行脱糖.2.2 Rust 的引用变量类型众所周知, 生命周期的初衷, 就是 Rust 为了避免悬垂引用而整出来的工具.而两个不同的生命周期, 在 rustc 的眼中, 它们的类型其实是不一样的. 也就是说, a str 和 b str 是两个不同的类型.这个时候有人就会问了, 那我们平时见到的对某个变量的 str 呢? 它们是同一个类型吗?我觉得不是(这也是上面编译报错的原因). 因为 str 其实只是 Rust 帮你省略了生命周期, 生命周期并不是不存在了.所以, 我们也知道, 生命周期它不会凭空产生, 也不会凭空消失. 毕竟你不可能凭空造出一个生命周期范围对不对.那假设引用变量当作一个函数的形参呢?因为函数它不一样, 函数面对生命周期可以说具体问题具体分析. 函数变量的生命周期它是不确定的.那有人说, 我这样行不行呢?// Code A:fn foo(a: a str, b: a str) - a str {}别忘了, 这个 a 只是我们暂时设的未知数! 它最终的结果是由编译器确定的, 也就是已知的! 这样子声明形参的话, 我们只知道 a 和 b 是一个类型的变量, 那编译器看到这就会很疑惑:诶? 这个生命周期变量 a 我没见到过啊? 什么鬼? 我不到啊! 报错, 报错!!!所以, 我们必须将 foo 改为 fooa, 这个代表我们从外部传递进来一个生命周期 a, 参数 a 和 b 的生命周期是这个 a. 所以, 在这个函数中, 参数 a 和参数 b 的生命周期是一样的.这个就是生命周期在形参的本质.那思考一下, 那为什么我们在单个引用形参的函数中, 一般不带生命周期呢? 例如fn foo(a: str) - str {}那我把它详细写出来, 变成这样可不可以?fn fooa(a: a str) - a str {}Perfect! 和上一段是一样的! 因为你没必要考虑多参数下的变量生命周期关系, 所以这一段和上一段完全等价.那既然如此, 我们还是选择上面的吧, 至少写起来简单点, 下面的一看就瘆人(笑)。The Rustonomicon Introduction:本知识“按原样”提供不作任何明示或暗示的保证包括但不限于保证释放无法形容的恐怖这些恐怖会粉碎你的精神让你的思想在未知无限的宇宙中漂流。好了, 这个时候就必须面对一个问题:那为什么fn debuga(a: a str, b: a str) {println!(“a {a:?} b {b:?}”);}fn main() {let hello: static str “hello”;{let world String::from(“world”);let world world; // world has a shorter lifetime than staticdebug(hello, world);}}这样的代码也能通过呢?明明 hello 的生命周期是 static, world 的生命周期是 world. 按照道理, 它们的生命周期并不一样而 debug 函数要求两个参数的生命周期严格等于 a.它们应该不通过啊? 那这是什么问题呢?这个说来话长…生命周期的变异性(协变, 逆变, 不变)我们先来看上面的 fooa 函数.我们给 foo 加上了生命周期模板 a, 那这个 a 肯定是外部传递进来的.然后我们来看这么一段脱糖后的调用代码:fn main() {let hello: static str “hello”;world: { // 错误示范, 对应 The Rustonomicon 的脱糖代码let world String::from(“world”);let world world;let c: x foox(static hello, world world);}println!(“{c}”);}3.1 a 到底是什么?但是这个 a 到底是什么呢?我们可以看到, 假设 x 的生命周期不是 world 而是 static, 那么这段代码会瞬间 UAF, 然后收到 Linux 的黄牌警告.同理, 如果我们将 world 和 hello 反一下, 结果是一样的, 生命周期保护成了摆设.所以, 为了防止这种事情的发生, 我们必须让 x 的生命周期位于 world 中.由此我们可以看出, 传入的生命周期必须为对应参数的交集, 否则就有可能出现 UAF.3.2 子类型这个不多讲, The Rustonomicon 中都有, 简单略过.假设有两个生命周期变量 life1 和 life2如果 life2 存活时间包含在 life1 中那么我们把 life1 称为 life2 的子类型.3.3 协变那 static 和 world 明明不一样, 为什么它们仍然能被当作函数中的 a 被传递进去呢? 这就涉及到一个关键点–生命周期协变.在 The Rustonomicon 中, 协变是这么定义的:给定两个类型 Sub 和 Super其中 Sub 是 Super 的子类型:F 是协变的如果 F 是 F的子类型.吓哭了, 隔着屏幕都能感受到那不可名状的恐怖.别慌, 我们来解构一下.在 3.1 我们讲过, 实际传入的 x world. 那么, 根据子类型的定义:令 F static str, F world str, Sub static, Super world因为 static 为 world 的子类型所以 static str 为 world str 的子类型所以 static str 可以被自动转换成 world str所以, 这段代码let c: x foox(static hello, world world);其实等价于下面这段代码let c: world fooworld(world hello, world world);像这种因为 static 为 world 的子类型, 所以 static str 为 world str 的子类型, 所以可以安全转换的, 我们叫做 变量的协变.3.4 不变来看位于 The Rustonomicon 的下面一段代码:fn assign(input: mut T, val: T) {*input val;}fn main() {let mut hello: static str “hello”;{let world String::from(“world”);assign(mut hello, world);}println!(“{hello}”);}老办法, 我们进行脱糖fn assigna, T(input: a mut T, val: T) {*input val;}fn main() {let mut hello: static str “hello”;world: {let world String::from(“world”);life1: {assignlife1, world str(life1 mut static hello, world world);}}println!(“{hello}”);}这一看就是出问题的节奏啊. 类似 C 的指针一样, 我们传递了 input(static str) 的引用, 然后改变了 input 的值.如果我们将这个函数的 T 泛化为一个引用, 那么 T 本质上就是 C 语言的一个二级指针, 而 assign 函数其实就是对这个二级指针进行解引用和赋值.问题是, 这段代码它也会 UAF! 因为你将一个存活期较小的变量赋值给了一个存活期较大的变量!在这个例子中:input 的类型是 static strworld 的类型是 world strworld 的生命周期小于 input 的生命周期!那么, 当代码运行到 println 的时候, world 变量已经被释放掉了!UAF!所以, 我们可以看到, 若 T 为引用类型, 纵然 a T 的生命周期 a 被协变了, 但是 T 是不能变成比它更小的生命周期的.这个时候, 我们把 T(b str) 看作 Sub, a T(a b str) 看作 F, Super 同理.像这种 Sub 为 Super 的子类型, 但是 F却并不能看作 F 的子类型的, 我们把它叫做不变.3.5 逆变这个主要考虑很特殊的一种情况: 函数指针, 也就是 fn(x: a str) - ().道理其实很简单. 我们对比两个函数签名:fn f1(x: a str);fn f2(x: static str);假设我定义一个 fn(x: a str), 传入 f1, f2. 那这个 a 可以处理 static 的生命周期吗?当然可以!像这种因为 a 为 static 的子类型, 但是 static str 为 a str 的子类型, 所以可以安全转换的, 我们叫做 变量的逆变.The End