【Rust中级教程】1.12. 生命周期(进阶) Pt.2:生命周期变型、协变、不变、逆变

📅 2026/7/24 22:45:30
【Rust中级教程】1.12. 生命周期(进阶) Pt.2:生命周期变型、协变、不变、逆变
1.12 生命周期(进阶) Pt.2生命周期变型(Lifetime Variance)、协变(covariant)、不变(invariant)、逆变(contravariant)这篇文章在已有基础上对生命周期这一概念进行补充上篇见 1.11. 生命周期(进阶) Pt.1。1.12.1. 生命周期变型(Lifetime Variance)变型(Variance)是Rust类型系统中的一个概念它描述了泛型参数特别是生命周期参数在类型层次结构中的继承关系。我们可以简单地理解为变型是用于描述哪些类型是其他类型的“子类”的这里的“子类”比较类似于Java和C#中的子类。除此以外变型还会关注什么时候“子类”能够替换“超类”反之亦然。通常来说如果A是B的子类那么A至少和B一样有用。举一个Rust语言的例子如果函数接收a str的参数那么就可以传入static str的参数。因为static是a的子类static至少跟任何a存活的时间一样长static能够在程序运行中一直保持有效。1.12.2. 生命周期的3种变型所有类型都有变型每个类型所对应的变型定义了哪些类似类型可以用在该类型的位置上。注意以下的内容比较难建议你先回忆一下高中学的充分条件、必要条件等知识1. 协变(covariant)协变(covariant)指的是某类型只能用“子类型”来代替。协变表示如果 A : BA是B的子类型那么 FA : FBFA也是FB的子类型这是一个从小到大继承关系的传递类似于充分条件的推导如果 A 成立则 B 一定成立A 是 B 的充分条件。例如static T可以代替a T因为T是对a这个生命周期的协变所以a可以由它的子类比如说static来替代。2. 不变(invariant)不变(invariant)意味着必须提供指定的类型。不变表示A : B 不能推导出 FA : FB 也不能 FB : FA这意味着FA和FB之间没有足够的关系无法形成推导关系因此它们既不是充分条件也不是必要条件它们是独立的。比如说mut T这个可变引用对于T来说就是不变的。3. 逆变(contravariant)逆变表示如果 A : BA是B的子类型那么 FB : FAFB反而是FA的子类型这里的逻辑是 “想要FA成立B必须满足A的条件”更像必要条件如果 B 成立则 A 也必须成立A 是 B 的必要条件。你可以把逆变理解为“成反比”函数吧对参数的要求越低参数可发挥的作用越大。举两个例子- 假如有两个变量x1和x2其中x1的生命周期是staticx2的生命周期是a。那么毫无疑问x1的作用比x2大因为它活得更长。假如有两个函数take_func1和take_func2其中take_func1接收的参数是static strtake_func2接收的参数是a str。毫无疑问take_func1对参数的要求比take_func2严格这就导致了take_func1没有take_func2作用大。由上述的两个例子看出给变量声明一个更长的生命周期会使它的作用更大但是要求函数的参数是更长的生命周期就会使函数的作用更小。这就是所谓的逆变。那么这叫谁和谁的逆变呢就是函数对它里面的参数类型的逆变。1.12.3. 生命周期变型的作用我们通过一个例子来看一下生命周期变型的作用struct MutStra, b { s: a mut b str, } fn main() { let mut s hello; *MutStr { s: mut s }.s world; println!({}, s); }这个代码令人比较困惑的地方在于MutStr这个结构体我们来解析一下- 这个结构体只有一个字段但是拥有两个生命周期-a mut表示一个可变引用这个可变引用的生命周期是a-b str表示一个字符串切片的引用这个字符串切片的生命周期是b- 换句话说MutStr允许你存储一个可变引用这个引用指向一个字符串切片的引用。你可以修改s本身但不能修改b str指向的字符串内容接下来我们来看一下主函数的逻辑-let mut s hello;声明了s这个变量类型是str值是hello*MutStr { s: mut s }.s world;这其实是好几步被合在了一行我们分开来看MutStr { s: mut s }传递给MutStr结构体s的可变引用此时MutStr下的s字段的值就是hello*MutStr { s: mut s }.s world;的.s表示访问s字段此时这个字段的值是mut s。*解引用s即获得s这个字符串切片的引用本身。 world修改了指向的值——s之前指向hello现在被修改为world即s world。那如果只有一个生命周期还能这么写吗struct MutStra { s: a mut str, } fn main() { let mut s hello; *MutStr { s: mut s }.s world; println!({}, s); }输出error[E0308]: mismatched types -- src/main.rs:7:31 | 7 | *MutStr { s: mut s }.s world; | ----------------------- ^^^^^^^ expected str, found str | | | expected due to the type of this binding error[E0277]: the size for values of type str cannot be known at compilation time -- src/main.rs:7:5 | 7 | *MutStr { s: mut s }.s world; | ^^^^^^^^^^^^^^^^^^^^^^^ doesnt have a size known at compile-time | help: the trait Sized is not implemented for str note: the left-hand-side of an assignment must have a statically known size在这行合写的代码上rustc 把失败报在赋值处expected str, found str以及str不是Sized。更深层的类型问题是mut s的类型是mut str但字段期望的是mut str。具体来说- 变量s的类型是str引用一个字符串切片。- 当写mut s时其类型实际上是mut str即对变量s的可变引用。然而结构体MutStr的定义要求字段s的类型是mut str。- 如果把构造单独写成MutStr { s: mut s }rustc 则会报告不能把引用背后的数据再可变借用——这是同一类底层不匹配只是报错位置不同。这是所指类型不同而不是生命周期子类型问题。另外需要注意mut T确实支持 unsizing 强制转换例如mut [T; N]→mut [T]但这种机制仍然不能把mut str变成mut str。不变性真正起作用的地方是双生命周期版本a mut b str在b上是不变的这可以防止你通过可变引用赋值时以不安全的方式缩短内部借用。也可以这么理解- 字符串字面值hello和world就是字符串字面值是str类型有隐式的static生命周期标注也就是说str实际上是static str。原本的结构体里的b对应它- 结构体的a对应可变引用的生命周期也就是*MutStr { s: mut s }.s world这一行中的mut这个可变引用的生命周期- 修改之后的这个只有一个生命周期参数的结构体期望的是mut str但mut s的类型仍是mut str因此类型不匹配