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
35: Generic Functions and Structs
I've seen this happen to almost every developer I've mentored when they first hit generics. You want to write a utility function that works for any number—integers, floats, you name it—so you decide to stop repeating yourself and use a generic type T. It feels right until you try to actually do something with that data.
The 'T' that can't be compared
fn find_largest(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
If you try to compile this, Rust is going to scream at you. The error will be something along the lines of binary operation `>` cannot be applied to type `T`. It's a bit frustrating at first because, in your head, T is obviously a number. But to the compiler, T is anything. It could be a String, a custom User struct, or a TcpStream. Not everything in the universe can be compared with a "greater than" operator.
Giving Rust a hint with Trait Bounds
To fix this, we have to tell Rust that T isn't just any type—it's a type that specifically knows how to be compared. In Rust, we do this using trait bounds. For comparison, we use the PartialOrd trait.
fn find_largest(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Notice the <T: PartialOrd> syntax. I'm essentially telling the compiler: "This function works for any type T, as long as T implements the PartialOrd trait." Now the compiler is happy because it has a guarantee that the > operator actually exists for whatever type the caller passes in.
Structuring data for any type
Generics aren't just for functions; they're incredibly powerful for structs. Imagine you're building a system to track a "current value" and a "previous value" for some kind of sensor. You wouldn't want to write one struct for i32, one for f64, and another for u16.
Instead, you define the struct with a generic type parameter. I usually do this when I know the structure of the data is constant, but the specific type of the data might change depending on the use case.
struct History<T> {
current: T,
previous: Option<T>,
}
impl<T> History<T> {
fn update(&mut self, new_value: T) {
self.previous = Some(std::mem::replace(&mut self.current, new_value));
}
}
One thing to keep in mind: when you implement methods for a generic struct, you have to declare the generic type again after the impl keyword. It looks redundant—impl<T> History<T>—but you're telling Rust that the implementation is generic over T, not for one specific version of History.
I personally find that generics make code significantly more maintainable. Instead of having five different versions of a "wrapper" or "container," you have one source of truth. Just remember: the moment you need to perform an action on that generic type (like adding, comparing, or printing it), you'll need to reach for those trait bounds.
📋 Practical Task
Implementing a Generic Result Cache
You are building a caching system that stores the result of an expensive computation. You need to create a generic struct called CacheEntry<T> that stores a value of type T and a timestamp (represented as a u64) of when it was created.
Your task is to:
- Define the
CacheEntry<T>struct. - Implement a method
is_expired(&self, current_time: u64, ttl: u64) -> boolthat returns true if the difference betweencurrent_timeand the entry's timestamp is greater than thettl(time-to-live). - Implement a method
get_value(&self) -> &Tthat returns a reference to the cached value. - In your
mainfunction, instantiate oneCacheEntryfor aStringand another for ani32, and verify thatis_expiredworks for both.
There are no comments for now.