1.12.1. Lifetime Variance

Variance is a concept in Rust’s type system. It describes how generic parameters — especially lifetime parameters — relate to one another in the type hierarchy.

We can think of it simply as variance describes which types are “subtypes” of other types, where “subtype” is somewhat similar to the concept used in Java and C#.

In addition, variance also cares about when a “subtype” can replace a “supertype” and vice versa.

In general, if A is a subtype of B, then A is at least as useful as B. Here is a Rust example: if a function takes &'a str, then &'static str can be passed in. Because 'static is a subtype of 'a, 'static lives at least as long as any 'a (and 'static can remain valid for the entire program).

1.12.2. Three Kinds of Lifetime Variance

All types have variance. The variance associated with each type defines which similar types can be used in that type’s position.

Note: the following content is fairly difficult. It is recommended that you first recall the ideas of sufficient conditions and necessary conditions from high school math.

1. Covariant

Covariant means that a type can be replaced only by a “subtype.”

Covariance means:

if A <: B (A is a subtype of B), then F<A> <: F<B> (F<A> is also a subtype of F<B>)

Enter fullscreen mode Exit fullscreen mode

This is a transitive inheritance relationship from smaller to larger, similar to reasoning from a sufficient condition: if A holds, then B must also hold (A is a sufficient condition for B).

For example, &'static T can replace &'a T, because &T is covariant over the lifetime 'a, so 'a can be replaced by one of its subtypes, such as 'static.

2. Invariant

Invariant means that you must provide the exact specified type.

Invariance means:

A <: B cannot imply F<A> <: F<B>, and F<B> <: F<A> also cannot be inferred

Enter fullscreen mode Exit fullscreen mode

This means there is not enough relationship between F<A> and F<B> to derive one from the other, so they are neither sufficient conditions nor necessary conditions; they are independent.

For example, the mutable reference &mut T is invariant over T.

3. Contravariant

Contravariance means:

if A <: B (A is a subtype of B), then F<B> <: F<A> (F<B> is instead a subtype of F<A>)

Enter fullscreen mode Exit fullscreen mode

The logic here is: “To make F<A> hold, B must satisfy A’s condition,” which is more like a necessary condition: if B holds, then A must also hold (A is a necessary condition for B).

You can think of contravariance as “the relationship moves in the opposite direction”: the lower a function’s requirements for its parameters, the greater the range of cases it can handle.

Here are two examples:

  • Suppose there are two variables, x1 and x2, where x1 has the lifetime 'static and x2 has the lifetime 'a. Then clearly x1 is more useful than x2, because it lives longer.

  • Suppose there are two functions, take_func1 and take_func2, where take_func1 accepts &'static str and take_func2 accepts &'a str. Clearly, take_func1 places stricter requirements on its argument, which means take_func1 is not as broadly useful as take_func2.

From the two examples above, we can see that giving a variable a longer lifetime makes it more useful, but requiring a function parameter to have a longer lifetime makes the function less useful. That is contravariance.

So what is contravariant with what? It is the function’s contravariance over the types of its parameters.

1.12.3. The Role of Lifetime Variance

Let’s look at an example to see what lifetime variance does:

struct MutStr<'a, 'b> {  
    s: &'a mut &'b str,  
}  

fn main() {  
    let mut s = "hello";  
    *MutStr { s: &mut s }.s = "world";  
    println!("{}", s);  
}

Enter fullscreen mode Exit fullscreen mode

The confusing part of this code is the MutStr struct, so let’s break it down:

  • The struct has only one field, but it has two lifetimes
  • &'a mut means a mutable reference, and the lifetime of that mutable reference is 'a
  • &'b str means a reference to a string slice, and the lifetime of that string slice is 'b
  • In other words, MutStr lets you store a mutable reference that points to a reference to a string slice. You can modify s itself, but you cannot modify the string content pointed to by &'b str

Next, let’s look at the logic in main:

  • let mut s = "hello"; declares the variable s, whose type is &str, and whose value is "hello"

  • *MutStr { s: &mut s }.s = "world"; is actually several steps combined into one line. Let’s separate them:

    • MutStr { s: &mut s } passes a mutable reference to s into the MutStr struct. At this point, the value of the s field inside MutStr is "hello"
    • In *MutStr { s: &mut s }.s = "world";, the .s means access the s field (at this point the field’s value is &mut s). * dereferences s, that is, it obtains the reference itself to the string slice s. = "world" changes the pointed-to value — s used to point to "hello", and now it is changed to "world", that is, s = "world"

What if there were only one lifetime — could this still be written?

struct MutStr<'a> {  
    s: &'a mut str,  
}  

fn main() {  
    let mut s = "hello";  
    *MutStr { s: &mut s }.s = "world";  
    println!("{}", s);  
}

Enter fullscreen mode Exit fullscreen mode

Output:

error[E0308]: mismatched types
 --> src/main.rs:7:31
  |
7 |     *MutStr { s: &mut s }.s = "world";  
  |     -----------------------   ^^^^^^^ expected `str`, found `&str`
  |     |
  |     expected due to the type of this binding

error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/main.rs:7:5
  |
7 |     *MutStr { s: &mut s }.s = "world";  
  |     ^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `str`
  = note: the left-hand-side of an assignment must have a statically known size

Enter fullscreen mode Exit fullscreen mode

On this one-liner, rustc reports the failure at the assignment (expected str, found &str, and str is unsized). The deeper type problem is that &mut s has type &mut &str, while the field expects &mut str.

More specifically:

  • The variable s has type &str (a reference to a string slice).
  • When you write &mut s, its actual type is &mut &str, that is, a mutable reference to the variable s. However, the MutStr definition requires the field s to have type &mut str.
  • If you isolate the construction as MutStr { s: &mut s }, rustc instead reports that you cannot borrow the data behind an & reference as mutable — the same underlying mismatch, surfaced differently.

These are different referent types, not a lifetime-subtyping question. (Separately, note that &mut T does support unsizing coercions such as &mut [T; N]&mut [T]; that mechanism still cannot turn &mut &str into &mut str.)

What invariance does matter for is the two-lifetime version: &'a mut &'b str is invariant in 'b, which prevents unsoundly shortening the inner borrow when you assign through the mutable reference.

You can also think about it this way:

  • String literals ("hello" and "world" are string literals) have type &str and an implicit 'static lifetime annotation, which means &str is actually &'static str. In the original struct, this corresponds to 'b
  • The struct’s 'a corresponds to the lifetime of the mutable reference, which is the lifetime of the &mut mutable reference in the line *MutStr { s: &mut s }.s = "world"
  • After the change, the struct with only one lifetime parameter expects &mut str, but &mut s still has type &mut &str, so the types do not match