Course contentsShow
← Rust
Lesson 149 of 1,9102. The Ownership ModelFree lesson

Covariance and Contravariance in Lifetimes

Apply variance rules to subtyping and lifetime flexibility in generic programming.

In this lesson, you will learn how the Rust compiler decides if a lifetime can be "shortened" to fit into a generic slot, known as variance.

The Problem: When can a lifetime fit?

Imagine you have a function that expects a reference with a lifetime 'a. If you have a reference with a longer lifetime 'b (where 'b covers everything 'a does and more), can you pass 'b into that function?

This is the heart of covariance. If a type T is covariant over its lifetime 'a, it means that if 'longer is a subtype of 'shorter, then &'longer T is a subtype of &'shorter T. Simply put: if you need a reference that lasts for a while, you can safely pass in a reference that lasts even longer.

Covariance vs. Contravariance

Most types in Rust, like &'a T or &'a [T], are covariant. This is why the borrow checker feels "helpful"—it lets you pass a long-lived reference into a function that only expects a short-lived one. It’s safe because a long-lived reference will definitely exist for the duration of the shorter requirement.

Contravariance, however, is the exact opposite. It occurs in rare situations, most notably with function arguments. If you have a function that needs to accept any string that lives at least as long as 'a, a function that can handle any string (even ones with shorter lifetimes) is actually more flexible.

Consider this:

// If 'long : 'short
fn process<'a>(f: fn(&'a str)) {
    // This function can accept a reference that lives for 'a
}

Because the function pointer fn(&'a str) is contravariant over the lifetime 'a, you can pass a function that is actually more flexible than required.

Why does this matter?

You rarely need to define these manually, but understanding them explains why the compiler sometimes rejects code where you feel a reference "should" fit. When you use complex types or trait objects, the compiler tracks these subtyping relationships to ensure that you never accidentally use a piece of memory after its lifetime has expired.

Key Takeaway: Covariance allows you to safely provide a longer-lived reference where a shorter one is expected, while contravariance allows you to provide a more flexible function where a specific one is expected.