1.12.1. 生命周期的变型

变型是 Rust 类型系统中的一个概念。它描述了泛型参数(尤其是生命周期参数)在类型层次结构中如何相互关联。

我们可以简单地理解为变型描述了哪些类型是其他类型的“子类型”,其中“子类型”与 Java 和 C# 中的概念有些相似。

此外,变型还关心何时可以用“子类型”替换“超类型”,反之亦然

一般来说,如果 A 是 B 的子类型,那么 A 至少与 B 一样有用。下面是一个 Rust 示例:如果一个函数接受 &'a str,那么就可以传入 &'static str因为 'static'a 的子类型,所以 'static 的存活时间至少与任何 'a 一样长(并且 'static 可以保持整个程序的有效性)。

1.12.2. 三种生命周期变型

所有类型都具有变型。每个类型所关联的变型定义了哪些相似的类型可以在该类型的位置上使用。

注意:以下内容较为困难。建议您先回顾高中数学中的充分条件和必要条件。

1. 协变

协变意味着类型只能被“子类型”替换。

协变意味着:

if A <: B (A 是 B 的子类型),则 F<A> <: F<B>(F<A> 也是 F<B> 的子类型)

Enter fullscreen mode Exit fullscreen mode

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

例如,&'static T 可以替换 &'a T,因为 &T 对生命周期 'a 是协变的,因此 'a 可以被其子类型(如 'static)替换。

2. 不变

不变意味着必须提供完全指定的类型。

不变意味着:

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

Enter fullscreen mode Exit fullscreen mode

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

例如,可变引用 &mut TT 是不变的。

3. 逆变

逆变意味着:

if A <: B(A 是 B 的子类型),则 F<B> <: F<A>(F<B> 反而是 F<A> 的子类型)

Enter fullscreen mode Exit fullscreen mode

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

您可以将逆变理解为“关系朝相反方向移动”:函数对参数的要求越低,它能处理的范围就越大。

下面是两个例子:

  • 假设有两个变量 x1x2,其中 x1 的生命周期为 'staticx2 的生命周期为 'a。显然 x1x2 更有用,因为它的存活时间更长。

  • 假设有两个函数 take_func1take_func2,其中 take_func1 接受 &'static strtake_func2 接受 &'a str。显然,take_func1 对参数的要求更严格,这意味着 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);  
}

Enter fullscreen mode Exit fullscreen mode

这段代码中令人困惑的部分是 MutStr 结构体,让我们分解一下:

  • 该结构体只有一个字段,但有两个生命周期
  • &'a mut 表示可变引用,该可变引用的生命周期为 'a
  • &'b str 表示对字符串切片的引用,该字符串切片的生命周期为 'b
  • 换句话说,MutStr 允许您存储一个指向字符串切片引用可变引用。您可以修改 s 本身,但不能修改 &'b str 所指向的字符串内容

接下来,让我们看看 main 中的逻辑:

  • let mut s = "hello"; 声明了变量 s,其类型为 &str,值为 "hello"

  • *MutStr { s: &mut s }.s = "world"; 实际上是将几步操作合并为一行。让我们将它们分开:

    • MutStr { s: &mut s }s 的可变引用传入 MutStr 结构体。此时,MutStr 内部 s 字段的值是 "hello"
    • *MutStr { s: &mut s }.s = "world"; 中,.s 表示访问 s 字段(此时字段的值是 &mut s)。* 解引用 s,即获取指向字符串切片 s 的引用本身。= "world" 更改指向的值——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);  
}

Enter fullscreen mode Exit fullscreen mode

输出:

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";  
  |     ^^^^^^^^^^^^^^^^^^^^^^^ doesn't 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

Enter fullscreen mode Exit fullscreen mode

在这一行代码上,rustc 在赋值处报告失败(expected str, found &str,并且 str 的大小未知)。更深层次的类型问题是 &mut s 的类型是 &mut &str,而字段期望的是 &mut str

更具体地说:

  • 变量 s 的类型是 &str(对字符串切片的引用)。
  • 当您编写 &mut s 时,其实际类型是 &mut &str,即对变量 s 的可变引用。但是,MutStr 的定义要求字段 s 的类型为 &mut str
  • 如果单独构造 MutStr { s: &mut s },rustc 会报告无法将 & 引用背后的数据作为可变借用——这是同一个底层不匹配,以不同的方式显现。

这些是不同的所指类型,而不是生命周期子类型的问题。(另外,请注意 &mut T 确实 支持非确定大小的强制转换,例如 &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,因此类型不匹配