型变三法则:协变 vs 逆变 vs 不变

2025-05-03 · 约 6 分钟 · 标签: Rust , 类型理论 , 泛型 , 子类型

1. 协变(Covariance)

定义:如果 A 是 B 的子类型,那么 F<A> 也是 F<B> 的子类型。
方向:相同(A → B ⇒ F<A> → F<B>)。
典型例子:不可变引用 &T、Box<T>、Vec<T>。

🌰 例子:动物和狗

trait Animal { fn sound(&self); }
struct Dog;
impl Animal for Dog { fn sound(&self) { println!("Woof!"); } }

// 协变:&Dog 可以当作 &Animal 使用
fn make_sound(animal: &dyn Animal) {
    animal.sound();
}

fn main() {
    let dog = Dog;
    make_sound(&dog); // ✅ &Dog 协变为 &Animal
}

为什么安全?
因为 &T 是只读的,不可能通过 &Animal 修改数据,所以协变是安全的。


2. 逆变(Contravariance)

定义:如果 A 是 B 的子类型,那么 F<B> 是 F<A> 的子类型。
方向:相反(A → B ⇒ F<B> → F<A>)。
典型例子:函数参数(如 fn(T))。

🌰 例子:事件处理器

trait Event {}
struct ClickEvent;
impl Event for ClickEvent {}

// 逆变:fn(Event) 是 fn(ClickEvent) 的子类型
fn handle_event(event: impl Event) {
    println!("Event handled!");
}

// 接受一个处理 ClickEvent 的函数
fn set_click_handler(handler: fn(ClickEvent)) {
    handler(ClickEvent); // 调用时传入 ClickEvent
}

fn main() {
    // ✅ 将 fn(Event) 传给需要 fn(ClickEvent) 的函数
    set_click_handler(handle_event); // 逆变:父类型 => 子类型
}

为什么安全?
因为 handler 只需要处理 ClickEvent,而 handle_event 能处理所有 Event,所以传入更通用的函数是安全的。


3. 不变(Invariance)

定义:F<A> 和 F<B> 没有任何子类型关系,即使 A 和 B 有。
典型例子:可变引用 &mut T、Cell<T>。

🌰 例子:可变引用的危险

trait Animal {}
struct Dog;
struct Cat;
impl Animal for Dog {}
impl Animal for Cat {}

// 尝试让 &mut Dog 协变为 &mut Animal(会失败!)
fn train_animal(animal: &mut dyn Animal) {
    // 如果允许协变,这里可能把 Cat 写进 Dog 的引用!
}

fn main() {
    let mut dog = Dog;
    train_animal(&mut dog); // ❌ 编译错误(Rust 禁止)
}

为什么不安全?
如果允许 &mut Dog 当作 &mut Animal 使用,train_animal 可能会把 Cat 赋值给 dog,导致类型混乱!


对比表

特性方向例子Rust 中的典型场景
协变A → B ⇒ F<A> → F<B>&Dog 当作 &Animal&T, Box<T>, Vec<T>
逆变A → B ⇒ F<B> → F<A>fn(Event) 传给 fn(ClickEvent)函数参数 (fn(T))
不变无关系&mut Dog ≠ &mut Animal&mut T, Cell<T>

关键记忆点

  1. 协变:子类型关系正向传递(安全场景:只读数据)。
  2. 逆变:子类型关系反向传递(函数参数,保证输入足够具体)。
  3. 不变:禁止传递(可变数据必须严格匹配类型,防止错误写入)。

在 Rust 中,编译器会自动推断这些规则,你只需要在写泛型或生命周期复杂的代码时注意它们的影响!

评论
评论功能尚未配置:在 GitHub 仓库开启 Discussions,并按项目 README 里的步骤填好 src/consts.ts 即可启用(基于 Giscus,评论需要 GitHub 账号)。