Type System Fundamentals

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