Skip to Content
Course content

137: Variance in Rust's Type System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Wait, what does "variance" actually mean when talking about lifetimes?

In most languages, you think of subtyping in terms of classes—like a Cat being a subtype of Animal. In Rust, we don't have class inheritance, but we do have subtyping with lifetimes. Specifically, if a lifetime 'long outlives 'short, then 'long is a subtype of 'short. It's "more specific" or "more capable" because it lasts longer.

Variance is just a fancy word for how that subtyping relationship transfers to more complex types. If 'long is a subtype of 'short, is &'long T a subtype of &'short T? Yes. That's called covariance. It means the complex type "varies in the same direction" as the underlying type. I usually explain it as: "If I can use a long-lived reference where a short-lived one is expected, the type is covariant."

fn print_it(s: &str) { // effectively & 'static str or some shorter bound
    println!("{}", s);
}

fn main() {
    let long_lived: &'static str = "I live forever";
    // This works because &str is covariant over its lifetime.
    // We are passing a 'static lifetime where a shorter one is fine.
    print_it(long_lived);
}

Why is &mut T invariant? Why can't it just be covariant?

This is where things get tricky and where most people hit a wall. If &mut T were covariant, we could accidentally create a memory safety hole. Let me show you why.

Imagine if you could pass a &mut &'static str into a function that expects a &mut &'short str. Inside that function, the code could replace the 'static reference with a 'short reference. But the original caller still thinks they have a &mut &'static str! They would then try to use that reference long after the 'short value had been dropped. That's a classic use-after-free bug.

To prevent this, Rust makes &mut T invariant. Invariance means the types must match exactly. No subtyping allowed. If the function wants &mut &'a str, you must give it exactly that—not something that lasts longer, and certainly not something that lasts shorter.

fn replace_with_short(slot: &mut &str) {
    let short_lived = String::from("I'm temporary");
    *slot = &short_lived; // Error if slot was covariant!
}

fn main() {
    let mut long_lived: &'static str = "I'm permanent";
    // If this were allowed, long_lived would now point to a dropped String
    // replace_with_short(&mut long_lived); 
}

How do I know if my own generic structs are variant or invariant?

You don't actually have to declare variance in Rust; the compiler figures it out for you based on how you use the type parameters. I like to think of it as a "leakage" problem. If a type parameter T only appears in "read-only" positions (like &T), the struct remains covariant.

But the moment you put T inside something invariant—like &mut T or UnsafeCell<T> (which is what Cell and RefCell use)—the entire struct becomes invariant over T. It's like a contagion; the invariance of the internal field "infects" the outer struct.

  • struct MyBox<T> { data: T } → Covariant.
  • struct MyWrapper<T> { data: &mut T } → Invariant.
  • struct MyContainer<T> { data: Vec<T> } → Covariant (because Vec is covariant).

If you ever find yourself fighting the borrow checker on a generic struct and you're sure the lifetimes are logically sound, check if you've accidentally introduced invariance by using a mutable reference or a Cell inside your struct.




📋 Practical Task

Exercise: Resolving Invariance in a Reference Wrapper

You are building a small caching utility. You've created a struct called CacheSlot that holds a mutable reference to a value. However, you've discovered that because of invariance, you cannot pass a CacheSlot containing a 'static reference into a function that expects a CacheSlot with a shorter lifetime.

The Goal: Modify the CacheSlot struct so that it is covariant over its lifetime parameter 'a, allowing the code to compile. To do this, you'll need to change how the data is stored—think about whether you actually need a mutable reference to the reference itself or if you can use a different pointer type.

struct CacheSlot<'a, T> {
    // This field makes CacheSlot invariant over 'a
    value: &mut &'a T, 
}

fn process_slot<T>(slot: CacheSlot<'_ , T>) {
    println!("Processing slot...");
}

fn main() {
    let static_str: &'static str = "Hello Variance";
    let mut mutable_ref = static_str;
    
    let slot = CacheSlot {
        value: &mut mutable_ref,
    };

    // This currently fails to compile because CacheSlot is invariant
    process_slot(slot);
}

Requirement: Change the definition of CacheSlot so that process_slot(slot) compiles, while still maintaining the ability to store a reference to T. Hint: Consider if &'a T is sufficient instead of &mut &'a T.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.