- Published on
型变三法则:协变 vs 逆变 vs 不变
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> |
关键记忆点
- 协变:子类型关系正向传递(安全场景:只读数据)。
- 逆变:子类型关系反向传递(函数参数,保证输入足够具体)。
- 不变:禁止传递(可变数据必须严格匹配类型,防止错误写入)。
在 Rust 中,编译器会自动推断这些规则,你只需要在写泛型或生命周期复杂的代码时注意它们的影响!
THE END