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
54: Smart Pointers: Box, Rc, and RefCell
Up until now, we've leaned heavily on Rust's ownership rules: one owner, one mutable reference, or many immutable references. For 90% of your code, that's perfect. But eventually, you'll hit a wall where the compiler tells you that your data structure is "too large" or that you can't have two things owning the same piece of memory. This is where smart pointers come in. They aren't just pointers; they're structs that manage memory for you using the Drop trait.
The problem with recursive sizes and why Box is the escape hatch
Imagine you're building a simple linked list or a tree of categories for a file system. You might try to define a Category struct that contains a list of sub-categories. If you try to put a Category directly inside another Category, the compiler will scream at you. Why? Because it can't calculate the size of the struct at compile time—it's like a Russian nesting doll that never ends.
// This will fail to compile
struct Category {
name: String,
sub_category: Option<Category>,
}
The "naive" mistake is trying to fight the type system by making everything optional or trying to use raw pointers. The right way is to use Box<T>. A Box is the simplest smart pointer; it just puts the data on the heap. Because the Box itself is just a pointer, it has a fixed size regardless of what it's pointing to. By wrapping our sub-category in a Box, we tell Rust: "The category is somewhere else on the heap; just keep the address here."
struct Category {
name: String,
sub_category: Option<Box<Category>>,
}
I usually think of Box as the tool for "indirection." Use it when you have a recursive type or when you have a massive struct that you don't want to keep copying around the stack.
When one owner isn't enough: Entering the world of Rc
Now, let's say you're building a graph of game entities. You have a Player and an Enemy, and both of them need to reference a shared Team object to check for friendly fire. If you use Box, only one of them can own the Team. If you try to pass a reference, you'll run into lifetime nightmares that make you want to quit programming for the day.
This is where Rc<T> (Reference Counted) comes in. Unlike Box, Rc allows multiple ownership. It keeps a counter of how many people are holding a pointer to the data. When the counter hits zero, the data is finally cleaned up. It's a bit like a library book: multiple people can have a record of it, but the book only leaves the library when nobody is using it anymore.
use std::rc::Rc;
struct Team {
name: String,
}
let shared_team = Rc::new(Team { name: "Red Team".to_string() });
let player_ref = Rc::clone(&shared_team);
let enemy_ref = Rc::clone(&shared_team);
// Now both player_ref and enemy_ref point to the same Team on the heap.
The catch? Rc pointers are immutable. You can have a hundred Rc pointers to that Team, but none of them can change the team's name. If you try to mutate the data inside an Rc, the compiler will shut you down immediately. This is where most developers get stuck.
Breaking the rules with RefCell and Interior Mutability
So, what if you need multiple owners and you need to be able to mutate the data? You can't just wrap Rc in a mut variable because that only makes the pointer mutable, not the data it points to. This is where RefCell<T> comes in. It provides something called "interior mutability."
Basically, RefCell moves the borrowing rules from compile-time to runtime. Instead of the compiler checking if you have too many references, RefCell checks at runtime. If you try to borrow it mutably while someone else is already reading it, your program will panic. It's a trade-off: you lose some compile-time safety in exchange for flexibility.
In practice, we almost always see these two used together: Rc<RefCell<T>>. This pattern is the "gold standard" for shared, mutable state in a single-threaded Rust program.
use std::rc::Rc;
use std::cell::RefCell;
struct Team {
name: String,
}
let shared_team = Rc::new(RefCell::new(Team { name: "Red Team".to_string() }));
// We can borrow the interior mutably via .borrow_mut()
shared_team.borrow_mut().name = "Blue Team".to_string();
// Everyone holding an Rc now sees the updated name
println!("Team is now: {}", shared_team.borrow().name);
I'll be honest: using RefCell too much is often a sign that your architecture is a bit messy. If you find yourself wrapping every single struct in Rc<RefCell<T>>, you're basically writing Java in Rust. But for graphs, caches, and complex observers, it's an indispensable tool. Just remember: Box for heap/recursion, Rc for sharing, and RefCell for changing that shared data.
📋 Practical Task
Exercise: Implementing a Shared Configuration Registry
You are building a system where multiple components (a DatabaseConnector and an ApiClient) need to share a single AppConfig object. The configuration needs to be updateable at runtime (e.g., changing a timeout value) and those changes must be reflected across all components immediately.
Requirements:
- Create a struct
AppConfigwith a fieldtimeout: u32. - Create a struct
DatabaseConnectorandApiClientthat both hold a reference to theAppConfig. - Use
RcandRefCellto ensure both components share the same instance and can see updates. - In your
mainfunction:- Initialize the
AppConfigwith a timeout of 30. - Create both the connector and the client.
- Print the timeout from the
DatabaseConnector. - Update the timeout to 60 using the
ApiClient. - Print the timeout from the
DatabaseConnectoragain to verify the change was reflected.
- Initialize the
There are no comments for now.