基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

什么是生命周期子类型化(Lifetime Subtyping)或变变性(Variance)?

知识点图片

这是一个非常深入且核心的 Rust 概念(虽然在其他类型系统中也存在,但在 Rust 中尤为重要)。这两个概念解释了为什么有些代码可以通过编译,而有些看似合理的代码却会被借用检查器(Borrow Checker)拒绝

简单来说:

  1. 生命周期子类型化(Lifetime Subtyping):定义了生命周期之间的长短关系(谁能替代谁)。
  2. 变变性(Variance):定义了当生命周期或类型作为参数组合成更复杂的类型(如 &'a TVec<T>)时,这种子类型关系是如何传递(或不传递)的。

1. 生命周期子类型化 (Lifetime Subtyping)

在面向对象编程中,如果 CatAnimal 的子类,那么在需要 Animal 的地方,你可以传入 Cat。这叫里氏替换原则

在 Rust 中,生命周期也有这种关系:

  • 规则:如果生命周期 'long 比生命周期 'short 活得更长(即 'long 包含 'short),那么 'long 就是 'short子类型
  • 语法'long: 'short(读作:'long outlives 'short)。

为什么长的是子类型?
因为子类型必须能替代父类型。

  • 如果一个函数需要一个活 5 秒的引用('short)。
  • 你给它一个活 10 秒的引用('long)。
  • 这是安全的,因为 10 秒 > 5 秒。
  • 所以,'long 可以替代 'short'long 是子类型。

2. 变变性 (Variance)

变变性描述的是:如果 AB 的子类型,那么 F<A>F<B> 是什么关系?

在 Rust 中,主要有三种变变性:

A. 协变 (Covariance) —— "顺着变"

这是最常见的情况。

  • 定义:如果 AB 的子类型,那么 F<A> 也是 F<B> 的子类型。
  • Rust 例子:不可变引用 &'a T
    • 因为 'long'short 的子类型。
    • 所以 &'long T&'short T 的子类型。
  • 实际意义:你可以把一个长生命周期的引用传给一个要求短生命周期的函数。

B. 逆变 (Contravariance) —— "反着变"

这在 Rust 中比较少见,主要出现在函数的参数中。

  • 定义:如果 AB 的子类型,那么 F<B>F<A> 的子类型(关系反过来了)。
  • Rust 例子:函数指针 fn(T)
    • 假设你需要一个函数,它能处理活 10 秒的数据 fn(&'long T)
    • 你能不能传给它一个只能处理活 5 秒数据的函数 fn(&'short T)不能,因为万一你给它传了 10 秒的数据,它可能只处理 5 秒就失效了(或者反过来理解:函数要求的条件越宽松,它越通用)。
    • 实际上,如果你需要一个处理 'short 的函数,你可以传入一个能处理 'long 的函数吗?也不行。
    • 逆变的直观理解:如果你需要一个能处理 Animal 的函数,你可以传入一个能处理 Cat 的函数吗?不行,因为调用者可能会传入 Dog。但你可以传入一个能处理 LivingThing(更宽泛/父类型)的函数。
    • 所以在函数参数中,要求越低(父类型)的函数,反而可以替代要求高(子类型)的函数

C. 不变 (Invariance) —— "不能变"

这是导致 Rust 新手最头疼的地方,通常出现在可变引用中。

  • 定义:即使 AB 的子类型,F<A>F<B> 也没有任何关系,谁也不能替代谁。必须完全精确匹配。
  • Rust 例子:可变引用 &'a mut T

3. 为什么 &mut T 必须是“不变”的? (关键点)

这是理解 Variance 最重要的例子。为什么 &'long mut T 不能当作 &'short mut T 使用?

假设 &mut T 是协变的(允许长替短),看看会发生什么灾难:

plaintext
fn evil_function(s: &mut &'short str, l: &'long str) {
    // 假设 s 原本是一个 &'long str 的可变引用
    // 但因为协变,我们把它当成了 &'short str 的可变引用传入
    
    // 我们把一个短生命周期的字符串赋值给了 s
    *s = l; // 这里的 l 实际上如果是一个更短的生命周期怎么办?
}

fn main() {
    let long_lived_string = String::from("Hello");
    let mut long_ref: &'static str = "static"; // 长生命周期引用
    
    {
        let short_lived_string = String::from("World");
        
        // 假设这是允许的(协变):
        // 我们把 &mut &'static str 传给了一个接受 &mut &'short str 的函数
        // 函数内部把 short_lived_string 赋值给了 long_ref
        // evil_function(&mut long_ref, &short_lived_string); 
    } 
    // short_lived_string 在这里死掉了
    
    // 灾难发生:long_ref 现在指向已经被释放的内存!
    println!("{}", long_ref); 
}

逻辑解析:

  1. 读取数据时:协变是安全的(长的可以当短的读)。
  2. 写入数据时:协变是不安全的。如果你把一个长生命周期的容器(&mut T)伪装成短生命周期的容器,你就可以往里面塞入一个短命的数据。
  3. 当那个短命数据死亡后,原来的长生命周期容器里就留下了一个悬垂指针

因此,Rust 强制规定:&mut T 对 T 是不变的(Invariant)。你必须提供确切的生命周期,不能自动转换。


4. 总结表

类型构造器 例子 变变性 (Variance) 含义
不可变引用 &'a T 协变 (Covariant) 'aT 都是协变。长命引用可替代短命引用。
可变引用 &'a mut T 协变 (对 'a)
不变 (对 T)
引用本身的生命周期可以缩短,但指向的数据类型 T 不能变
Box, Vec Box<T>, Vec<T> 协变 拥有所有权的数据结构通常是协变的。
UnsafeCell UnsafeCell<T> 不变 (Invariant) 内部可变性必须是不变的,防止数据竞争和悬垂指针。
函数参数 fn(T) 逆变 (Contravariant) 接受更宽泛类型的函数可以替代接受具体类型的函数。

一句话总结

  • 生命周期子类型化:活得长的 ('long) 是活得短的 ('short) 的子类型。
  • 协变(绝大多数情况):可以用长的替代短的(如 &T)。
  • 不变&mut T):必须完全一致,为了防止你往长命容器里塞短命数据。
00:00
00:00