Rust in Depth

variance (covariance / contravariance / invariance)

Here is a puzzle. If a reference that lives for a long region 'long is valid, can you use it where a reference living for a shorter region 'short is expected? Intuitively yes — something that outlives a short window certainly survives the window. So a long-lived borrow can stand in for a short-lived one. Rust models this with subtyping between lifetimes: 'long is a subtype of 'short when 'long outlives 'short. Variance is the set of rules that decide, when a lifetime sits inside a bigger type (inside &T, Vec<T>, fn(T), Cell<T>, and so on), whether that subtyping is allowed to carry through, and in which direction.

There are three outcomes, and you really only need them at a glance. Covariance is the natural, same-direction case: if 'long is a subtype of 'short, then &'long T is usable where &'short T is wanted. Most read-only containers are covariant in their lifetime and type, which is why you can pass a longer-lived reference to a function that asked for a shorter-lived one. Contravariance is the reversed-direction case, and it shows up only in function arguments: a function that accepts a SHORTER-lived reference is more general, so fn(&'short T) can be used where fn(&'long T) is expected — the flip happens because what you put INTO a function flows opposite to what you get out. Invariance means no substitution either way is allowed: the lifetime must match exactly. This is forced by anything that lets you both read and write the inner type, such as &mut T or Cell<T> — because if you could substitute, you could read at one lifetime and write at another and smuggle a short-lived reference into a long-lived slot, breaking soundness.

Why this matters: variance is almost entirely invisible — the compiler computes it automatically from how each type is built, and you never write the word. You meet it only when an error complains that two lifetimes must be exactly equal and you expected one to flex into the other; the culprit is usually a &mut or an interior-mutability type forcing invariance. The honest takeaway is conceptual: read-only positions are covariant (longer-lived can substitute), function-argument positions are contravariant (the flip), and anything mutable is invariant (no substitution). You rarely steer it by hand, but knowing it exists explains otherwise-baffling lifetime errors.

// Covariant: a longer-lived &str fits where a shorter-lived one is wanted. fn use_short(s: &str) { println!("{s}"); } let literal: &'static str = "forever"; // lives the whole program use_short(literal); // OK: &'static substitutes for any &'a // Invariant: &mut T pins the lifetime; no flexing either way. // fn(x: &mut &'short i32) cannot accept &mut &'long i32 — soundness forbids it.

Read-only positions flex (covariant); mutable positions are rigid (invariant) — and that rigidity is what keeps the type system sound.

You never declare variance; the compiler derives it. You feel it only as a lifetime error demanding exact equality — almost always because a &mut T or an interior-mutability type made the position invariant.

Also called
covariancecontravarianceinvariancesubtyping variance共變逆變型別變異