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
39: Practice Exercise: Building a Generic Stack
When you first start building generic containers in Rust, it's easy to think that T is a magic wand that just "works" for everything. But the compiler is a lot more pedantic than that. I remember writing my first generic stack and getting absolutely hammered by the borrow checker and trait requirements because I assumed T inherited some basic capabilities.
Take a look at this snippet. It looks logically sound, but it won't even compile:
struct Stack<T> {
elements: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { elements: Vec::new() }
}
fn push(&mut self, item: T) {
self.elements.push(item);
}
fn pop(&mut self) -> T {
self.elements.pop().unwrap()
}
fn print_all(&self) {
for item in &self.elements {
println!("Item: {}", item);
}
}
}
The "T cannot be formatted" wall
If you try to compile this, the compiler will scream at you inside the print_all method. It'll tell you that T doesn't implement std::fmt::Display. This is a classic "aha!" moment. By saying Stack<T>, you're telling Rust: "This stack can hold absolutely anything."
The problem is that not "absolutely anything" can be printed to the console. A custom struct you wrote without a Display implementation, or a raw pointer, can't be used with println!. Rust won't let you write a method that might fail depending on what T ends up being. To fix this, we have to add a trait bound to the specific implementation block where the printing happens.
// We only implement print_all for types that actually support Display
impl<T: std::fmt::Display> Stack<T> {
fn print_all(&self) {
for item in &self.elements {
println!("Item: {}", item);
}
}
}
Notice that I didn't put the bound on the struct definition itself. If I did, you'd never be able to create a Stack of something that isn't printable. By putting the bound on the impl block, the stack remains generic, but the print_all method only exists for types that satisfy the Display requirement. I find this is almost always the better architectural choice.
Replacing the panic with Option
Now look at the pop method. I used .unwrap(). In a quick prototype, that's fine. In a real project, it's a ticking time bomb. If you call pop() on an empty stack, your entire thread crashes. Since we're building a proper data structure, we should embrace Rust's Option type.
The Vec::pop method already returns an Option<T>. The simplest and most "Rustacean" thing to do is to just pass that Option right back to the caller. This shifts the responsibility to the user of the stack to decide how to handle the empty state, which is exactly where that decision belongs.
fn pop(&mut self) -> Option<T> {
self.elements.pop()
}
With these two changes, we have a stack that is truly flexible, safe from crashes, and respects the trait system. You're no longer fighting the compiler; you're using it to define the contract of your data structure.
📋 Practical Task
Building a Generic Undo Buffer for String Edits
You are tasked with creating a system that tracks a history of strings (like a simple text editor's undo history). Instead of a basic stack, you need to build an UndoBuffer that manages a stack of strings but also tracks the total number of "actions" performed.
Requirements:
- Create a generic struct
UndoBuffer<T>that contains aVec<T>and ausizecounter calledaction_count. - Implement a
push_action(&mut self, action: T)method that adds the item to the stack and increments the counter. - Implement an
undo(&mut self) -> Option<T>method that removes the last action from the stack and decrements the counter (ensure the counter doesn't underflow). - Implement a
get_history_size(&self) -> usizemethod that returns the currentaction_count. - Create a second
impl<T: std::fmt::Debug> UndoBuffer<T>block with a methoddebug_print_history(&self)that prints the entire stack using the{:?}formatter.
Test your implementation by creating an UndoBuffer<String>, pushing three different strings, calling undo once, and then printing the remaining history and the action count.
There are no comments for now.