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
67: Understanding Rust's Type System Variance
Variance is one of those terms that sounds like it belongs in a graduate-level type theory paper, and for a long time, I ignored it because Rust usually handles it for me. But eventually, you'll hit a wall where the compiler tells you that a type isn't what you think it is, even though the lifetimes seem to align perfectly. That's usually where variance is hiding.
In the simplest terms, variance describes how the subtyping of a complex type (like Vec<T> or &T) relates to the subtyping of the inner type T. In Rust, the only real subtyping we have is with lifetimes: if lifetime 'a outlives 'b, then 'a is a subtype of 'b. Essentially, a longer lifetime can always be used where a shorter one is expected.
The intuitive win: Covariance
Most of the time, Rust is covariant. If you have a function that expects a reference with a short lifetime, you can pass it a reference with a longer lifetime without a second thought. I'll show you a quick example of this working as intended:
fn print_it(s: &'a str) {
println!("{}", s);
}
let static_str: &'static str = "I live forever";
print_it(static_str); // This works perfectly.
This is covariance in action. Because 'static is a subtype of 'a, &'static str is a subtype of &'a str. The compiler is happy because the data is guaranteed to live at least as long as the function needs it. It's a safe, one-way street.
Where the intuition breaks: The danger of &mut
Now, here is where things get weird. You might assume that if &T is covariant, then &mut T is also covariant. It feels logical: if I have a mutable reference to a long-lived string, I should be able to use it where a mutable reference to a short-lived string is expected, right? Wrong. &mut T is invariant.
Let's look at the "naive" way we'd want this to work and why it would actually crash your program if Rust allowed it. Imagine we have a mutable reference to a static string, and we try to treat it as a mutable reference to a shorter-lived string so we can change what it points to.
// This is a hypothetical example of what WOULD happen if &mut was covariant
fn substitute_short(slot: &mut &'a str) {
let short_lived = String::from("I am short");
*slot = &short_lived; // Write a short lifetime into the slot
}
let mut long_lived: &'static str = "I am long";
substitute_short(&mut long_lived);
// Now long_lived is actually pointing to something that has been dropped!
println!("{}", long_lived); // MEMORY CORRUPTION!
If Rust allowed &mut &'static str to be treated as &mut &'a str, we could "poison" the long-lived reference by writing a short-lived reference into it. The owner of long_lived still thinks the reference is 'static, but we've swapped it for something that dies at the end of the function. This is why &mut T must be invariant; it's the only way to ensure that you can't sneak a shorter lifetime into a place where a longer one is required.
Choosing the right constraint
When you're designing your own structs, you'll find that Rust defaults to covariance whenever it can. If your struct just holds a reference, like struct Wrapper<'a> { data: &'a str }, it's covariant over 'a. You can pass a Wrapper<'static> into a function expecting Wrapper<'a>.
But the moment you add a mutable reference to that struct, the whole struct becomes invariant. I've seen colleagues get frustrated when a perfectly good struct suddenly starts throwing lifetime errors after they added a &mut field. The trade-off is strict: you get the ability to mutate the data, but you lose the ability to flexibly cast lifetimes. If you find yourself fighting invariance, ask yourself if you actually need a mutable reference inside the struct, or if you can use interior mutability (like Cell or RefCell) to decouple the mutation from the type's variance.
📋 Practical Task
Exercise: Fixing the Lifetime Poisoning in a Mutable Buffer
You are reviewing a piece of code for a colleague who is trying to implement a simple "Slot" system. They've written a function that is supposed to update a reference in a slot, but the compiler is refusing to let them pass a 'static reference into a function expecting a shorter lifetime.
Your Task:
Analyze the following code. You will see that the compiler rejects the call to update_slot because &mut &'a str is invariant. To fix this and make the code compile while maintaining safety, you must change the function signature of update_slot to accept a generic lifetime 'b that is constrained to outlive 'a, or adjust how the reference is handled. However, the real challenge is to realize that update_slot cannot simply "coerce" the variance.
Modify the code so that it compiles. You will need to ensure that the value being written into the slot lives at least as long as the slot's requirements. The goal is to make the long_lived variable correctly accept the update without violating Rust's invariance rules.
fn update_slot<'a>(slot: &mut &'a str, new_val: &'a str) {
*slot = new_val;
}
fn main() {
let mut long_lived: &'static str = "Initial Value";
{
let short_lived = String::from("Updated Value");
// ERROR: cannot coerce &mut &'static str to &mut &'a str
update_slot(&mut long_lived, &short_lived);
}
println!("Value: {}", long_lived);
}
Hint: Think about why the original code fails. The long_lived variable is 'static, but update_slot tries to treat the slot as having a shorter lifetime 'a. To fix this, the long_lived variable must be typed in a way that matches the expected variance, or the function must be generic enough to handle the specific lifetime of the input.
There are no comments for now.