Subtyping and Variance
Unlike object-oriented languages with class inheritance hierarchies, Rust has no subtyping between concrete structs or traits. Subtyping in Rust exists exclusively over lifetimes: a longer lifetime 'long is a subtype of a shorter lifetime 'short ('long: 'short), because anywhere a reference valid for 'short is expected, a reference valid for 'long (such as 'static) can safely be used.
Variance defines how subtyping of a lifetime 'long: 'short (or a type T) propagates through a type constructor $F\langle T \rangle$ (such as &'a T, &'a mut T, or fn(T)).
The Three Kinds of Variance in Rust
| Variance of $F\langle T \rangle$ | Rule When Sub is a Subtype of Super ('long: 'short) |
Common Type Constructors |
|---|---|---|
| Covariant | $F\langle\text{Sub}\rangle$ is a subtype of $F\langle\text{Super}\rangle$ (preserves direction) | &'a T (in both 'a and T), Box<T>, Vec<T>, Rc<T>, *const T, fn() -> T |
| Invariant | No subtyping relationship between $F\langle\text{Sub}\rangle$ and $F\langle\text{Super}\rangle$ (must match exactly) | &'a mut T (invariant in T, covariant in 'a), Cell<T>, RefCell<T>, Mutex<T>, *mut T |
| Contravariant | $F\langle\text{Super}\rangle$ is a subtype of $F\langle\text{Sub}\rangle$ (reverses direction!) | fn(T) (in argument position T) |
Given `'static: 'a` ('static outlives 'a):
- Covariant (`&'a T`): `&'static str` CAN be passed where `&'a str` is expected
- Invariant (`&mut T`): `&mut &'static str` CANNOT be passed where `&mut &'a str` is expected!
- Contravariant (`fn(T)`): `fn(&'a str)` CAN be passed where `fn(&'static str)` is expected
Why &'a T Is Covariant in T
Because &'a T is read-only, shortening the lifetime of a borrowed reference (&'static str $\rightarrow$ &'a str) is always safe:
fn print_short<'a>(a: &'a str, b: &'a str) {
println!("{a} and {b}");
}
fn main() {
let static_str: &'static str = "forever";
let local_string = String::from("temporary");
// `&'static str` is a subtype of `&'local str` (covariant), so both unify to `&'local str`:
print_short(static_str, &local_string);
}
forever and temporary
Why &'a mut T Must Be Invariant in T (Preventing Dangling Pointers!)
Why is &'a mut T invariant in T? Suppose &mut &'static str were allowed to coerce to &mut &'short str. A function accepting slot: &mut &'short str could then write a short-lived local reference into *slot, leaving the caller's &'static str variable pointing to destroyed stack memory! Invariance blocks this at compile time (E0597):
// ❌ does not compile
fn overwrite_with_short<'a>(slot: &mut &'a str, short_lived: &'a str) {
*slot = short_lived;
}
fn main() {
let mut static_ref: &'static str = "initial static";
{
let temp = String::from("dropped soon");
// Rejected because `&mut T` is INVARIANT in `T`:
// `T = &'static str` cannot shrink to `&'a str` behind `&mut`!
overwrite_with_short(&mut static_ref, &temp);
}
println!("{static_ref}");
}
error[E0597]: `temp` does not live long enough
--> src/main.rs:12:47
|
9 | let temp = String::from("dropped soon");
| ---- binding `temp` declared here
...
12 | overwrite_with_short(&mut static_ref, &temp);
| ----------------^^^^^-
| | |
| | borrowed value does not live long enough
| argument requires that `temp` is borrowed for `'static`
13 | }
| - `temp` dropped here while still borrowed