跳到主要内容

型变

问题

Rust 中的协变、逆变和不变是什么?它们如何影响泛型和生命周期?

答案

子类型关系

在 Rust 中,子类型关系只存在于生命周期之间:'long: 'short'long'short 的子类型),意味着 'long 至少活得和 'short 一样久。

fn example<'long: 'short, 'short>(
long_ref: &'long str,
short_ref: &'short str,
) {
// 'long 比 'short 活得久,所以 &'long str 可以当 &'short str 用
let _: &'short str = long_ref; // ✅ 协变
}

三种型变

型变含义直觉
协变(Covariant)F<'long>F<'short> 的子类型"方向一致"
逆变(Contravariant)F<'short>F<'long> 的子类型"方向相反"
不变(Invariant)无子类型关系"不能替换"

Rust 中的型变规则

类型'a 上的型变T 上的型变
&'a T协变协变
&'a mut T协变不变
*const T协变
*mut T不变
Box<T>协变
Vec<T>协变
Cell<T>不变
fn(T) -> UT 逆变,U 协变
UnsafeCell<T>不变

为什么 &mut TT 不变?

fn evil<'long: 'short, 'short>(
long_ref: &'long str,
slot: &mut &'short str, // 如果 &mut T 对 T 协变...
) {
// 假设允许:把 slot 的类型 "放宽" 为 &mut &'long str
// 然后写入一个更短生命周期的引用
*slot = long_ref; // 这看起来安全
}

// 但如果反过来:
fn evil2(slot: &mut &'static str) {
let local = String::from("temp");
// 如果 &mut T 协变,slot 可以当 &mut &'a str 用
// 就能把临时引用写进去 → 悬垂引用!
}

&mut TT 必须不变,否则可以通过可变引用写入更短生命周期的值,导致悬垂引用。

PhantomData 控制型变

use std::marker::PhantomData;

// 协变(默认)
struct Covariant<T> {
_marker: PhantomData<T>, // 协变于 T
}

// 逆变
struct Contravariant<T> {
_marker: PhantomData<fn(T)>, // 逆变于 T
}

// 不变
struct Invariant<T> {
_marker: PhantomData<fn(T) -> T>, // 不变于 T
}
// 或者
struct Invariant2<T> {
_marker: PhantomData<*mut T>, // 不变于 T
}

常见面试问题

Q1: 为什么函数参数是逆变的?

答案

直觉:如果一个函数接受 &'short str,那么一个接受 &'long str 的函数可以替代它吗?不可以——因为更严格(要求更长生命周期)。但一个接受更短生命周期的函数可以接受更长生命周期的输入。

// fn(&'static str) 可以用在需要 fn(&'a str) 的地方
// 因为 'static 更长,函数能处理更长的也能处理更短的

简单说:参数位置"消费"类型,所以方向相反(逆变);返回位置"产出"类型,方向一致(协变)。

Q2: Cell<T> 为什么对 T 不变?

答案

Cell<T> 允许通过共享引用修改内部值(内部可变性)。如果 Cell<T>T 协变,就可能通过 &Cell<&'long str> 写入一个 &'short str,导致生命周期不安全。本质原因和 &mut T 一样——可写就必须不变。

Q3: 普通开发中需要关心型变吗?

答案

大多数情况下不需要。编译器会自动处理。只有在以下场景需要理解型变:

  1. 编写包含裸指针的 unsafe 代码(需要用 PhantomData 标注正确的型变)
  2. 遇到复杂的生命周期编译错误时帮助理解原因
  3. 设计 zero-cost 抽象或底层库

相关链接