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

发布于:2025-02-19 ⋅ 阅读:(25) ⋅ 点赞:(0)

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)

这篇文章在Rust初级教程的基础上对生命周期这一概念进行了补充,建议先看【Rust自学】专栏的第10章的文章。
请添加图片描述

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 <: B(A是B的子类型),那么 F<A> <: F<B>(F<A>也是F<B>的子类型)

这是一个从小到大继承关系的传递,类似于充分条件的推导:如果 A 成立,则 B 一定成立(A 是 B 的充分条件)。

例如&'static T可以代替&'a T,因为&T是对'a这个生命周期的协变,所以'a可以由它的子类(比如说'static)来替代。

2. 不变(invariant)

不变(invariant)意味着必须提供指定的类型。

不变表示:

A <: B 不能推导出 F<A> <: F<B> 也不能 F<B> <: F<A>

这意味着F<A>F<B>之间没有足够的关系,无法形成推导关系,因此它们既不是充分条件,也不是必要条件,它们是独立的。

比如说&mut T这个可变引用对于T来说就是不变的。

3. 逆变(contravariant)

逆变表示:

如果 A <: B(A是B的子类型),那么 F<B> <: F<A>(F<B>反而是F<A>的子类型)

这里的逻辑是 “想要F<A>成立,B必须满足A的条件”,更像必要条件如果 B 成立,则 A 也必须成立(A 是 B 的必要条件)。

你可以把逆变理解为“成反比”:函数吧对参数的要求越低,参数可发挥的作用越大。

举两个例子:

  • 假如有两个变量x1x2,其中x1的生命周期是'staticx2的生命周期是'a。那么毫无疑问x1的作用比x2大,因为它活得更长。

  • 假如有两个函数take_func1take_func2,其中take_func1接收的参数是&'static strtake_func2接收的参数是&'a str。毫无疑问,take_func1对参数的要求比take_func2严格,这就导致了take_func1没有take_func2作用大。

由上述的两个例子看出,给变量声明一个更长的生命周期会使它的作用更大,但是要求函数的参数是更长的生命周期就会使函数的作用更小。这就是所谓的逆变。

那么这叫谁和谁的逆变呢?就是函数对它里面的参数类型的逆变。

1.12.3. 生命周期变型的作用

我们通过一个例子来看一下生命周期变型的作用:

struct MutStr<'a, '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这个字符串切片的引用本身。= "hello"修改了指向的值——s之前指向"hello",现在被修改为"world",即s = "world"

那如果只有一个生命周期还能这么写吗?

struct MutStr<'a> {  
    s: &'a mut str,  
}  
  
fn main() {  
    let mut s = "hello";  
    *MutStr { s: &mut s }.s = "world";  
    println!("{}", s);  
}

这段代码的问题在于类型不匹配和 Rust 中可变引用的不变性(invariance)。

具体来说:

  • 变量s的类型是&str(引用一个字符串切片)。
  • 当写&mut s时,其类型实际上是&mut &str,即对变量s的可变引用。然而,结构体MutStr的定义要求字段s的类型&mut str

虽然不可变引用(&T)支持“unsizing coercion”(例如可以将&[T; N]自动转换为&[T]),但是 可变引用 (&mut T) 是不变(invariant)的,这意味着Rust不允许自动将&mut &str转换为&mut str

因此,编译器会报类型不匹配的错误,因为它无法将&mut s(类型&mut &str)传给MutStr { s: ... }期望的&mut str参数。

在这里解释一下不变性:

  • 不可变引用 (&T) 是协变(variant)的:允许自动的unsizing转换,比如可以将&[T; N]当作&[T]使用
  • 可变引用 (&mut T) 是不变(invariant)的:这就意味着&mut &str&mut str被视为完全不同的类型,编译器不会自动进行转换

也可以这么理解:

  • 字符串字面值("hello"和"world"就是字符串字面值)是&str类型,有隐式的'static生命周期标注,也就是说&str实际上是&'static str。原本的结构体里的'b对应它
  • 结构体的’a对应可变引用的生命周期,也就是*MutStr { s: &mut s }.s = "world"这一行中的&mut这个可变引用的生命周期
  • 修改之后的这个只有一个泛型生命周期的结构体无法同时对应这两个生命周期标注,因为可变引用&mut T是不变(invariant)的,&mut &str&mut str被视为完全不同的类型,所以导致了报错

网站公告

今日签到

点亮在社区的每一天
去签到