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,
x1andx2, wherex1has the lifetime'staticandx2has the lifetime'a. Then clearlyx1is more useful thanx2, because it lives longer.Suppose there are two functions,
take_func1andtake_func2, wheretake_func1accepts&'static strandtake_func2accepts&'a str. Clearly,take_func1places stricter requirements on its argument, which meanstake_func1is not as broadly useful astake_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 mutmeans a mutable reference, and the lifetime of that mutable reference is'a -
&'b strmeans a reference to a string slice, and the lifetime of that string slice is'b - In other words,
MutStrlets you store a mutable reference that points to a reference to a string slice. You can modifysitself, 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 variables, 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 tosinto theMutStrstruct. At this point, the value of thesfield insideMutStris"hello" - In
*MutStr { s: &mut s }.s = "world";, the.smeans access thesfield (at this point the field’s value is&mut s).*dereferencess, that is, it obtains the reference itself to the string slices.= "world"changes the pointed-to value —sused 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
shas 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 variables. However, theMutStrdefinition requires the fieldsto 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&strand an implicit'staticlifetime annotation, which means&stris actually&'static str. In the original struct, this corresponds to'b - The struct’s
'acorresponds to the lifetime of the mutable reference, which is the lifetime of the&mutmutable 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 sstill has type&mut &str, so the types do not match
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.