Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
137: Variance in Rust's Type System
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 (becauseVecis 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.
There are no comments for now.