In this lesson, you will learn how to apply variance principles to correctly define generic lifetime bounds, ensuring your code remains flexible without violating memory safety.
The Concept of Variance
Variance describes how the subtyping relationship between two types (like a long-lived reference and a short-lived one) carries over when those types are used inside a generic structure. In Rust, we deal with three types of variance:
- Covariance: A type
Tis covariant if&'a Tcan be used where&'b Tis expected, provided'ais a subtype of'b(meaning'alives at least as long as'b). This is the default for shared references&T. - Contravariance: This happens when the relationship is reversed. It is rare in Rust but occurs with function signatures.
- Invariance: A type is invariant if no substitution is allowed. You must have an exact match. This is the default for mutable references
&mut T.
Why Variance Matters for Generics
Consider a struct that holds a reference. If you define struct Container<'a, T> { item: &'a T }, the compiler applies variance based on how you use T. If T is only used for reading, it is covariant. If it is used for writing (like in &mut T), it becomes invariant.
If you mistakenly treat an invariant type as covariant, you create a memory safety hole. For example, if you were allowed to shrink the lifetime of a mutable reference arbitrarily, you could cause a "use-after-free" by writing to a reference that has already been dropped.
Concrete Example
struct ReadOnly<'a, T> {
data: &'a T,
}
// Because this is read-only, it is covariant over 'a.
// You can pass a long-lived 'static reference where a shorter one is expected.
fn process(item: ReadOnly<'static, str>) {
let shorter_lived: ReadOnly<'_, str> = item;
}
When you define your own generic structs, Rust automatically calculates the variance for you. However, if you are building complex data structures, understanding that &mut T is invariant is critical: it prevents you from passing a &mut &'static str where a &mut &'a str is expected, because you could overwrite the static reference with a short-lived one.
Key Takeaway: Covariance allows flexibility in lifetimes for immutable reads, while Invariance forces strict lifetime matching for mutable writes to protect against dangling references.