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
36: Trait Bounds
Up until now, you've used generics to make your functions flexible. But you've probably already hit a wall where the compiler tells you that it doesn't know if a generic type T actually supports the operation you're trying to perform. That's where trait bounds come in. They're essentially how you tell Rust, "I don't care exactly what type this is, as long as it knows how to do X, Y, and Z."
Why can't I just use a generic T without any constraints?
If you write a function fn print_it<T>(item: T), you've told Rust that T can be anything. Literally anything. It could be an integer, a string, or a complex custom struct you wrote for a database connection. Because of that, Rust won't let you do anything with item that isn't universal to every single type in existence.
For example, if you try to print that item using println!("{:?}", item), the compiler will stop you. Why? Because not every type implements the Debug trait. To fix this, you add a trait bound. You're basically putting a requirement on the caller of the function.
use std::fmt::Debug;
// The ": Debug" part is the trait bound
fn log_value<T: Debug>(value: T) {
println!("The current value is: {:?}", value);
}
fn main() {
log_value(42); // Works! i32 implements Debug
log_value("Hello Rust"); // Works! &str implements Debug
// log_value(SomeCustomStructWithoutDebug); // This would fail to compile
}
How do I require a type to implement multiple traits?
In the real world, you often need a type to do more than one thing. Maybe you need a type that can be compared (so you can find the maximum) and also be printed (so you can log the result). You can chain these requirements using the + symbol.
I've found that this is where things start to get powerful. You aren't just restricting the type; you're defining a "capability profile" for your function. Check out this example where we want to find the "best" item in a list and print it.
use std::fmt::Display;
fn print_max<T: PartialOrd + Display>(list: &[T]) {
if let Some(max) = list.iter().max_by(|a, b| a.partial_cmp(b).unwrap()) {
println!("The highest value is: {}", max);
}
}
fn main() {
let numbers = vec![10, 50, 20];
print_max(&numbers); // Works: i32 is PartialOrd + Display
let words = vec!["apple", "zebra", "banana"];
print_max(&words); // Works: &str is PartialOrd + Display
}
When should I use a "where" clause instead of the angle brackets?
When you start adding multiple bounds to multiple generic types, your function signature starts to look like a mess of angle brackets. It becomes hard to actually see the function name and the arguments. This is why Rust provides the where clause. It doesn't change how the code works—it's purely for readability.
I generally switch to where clauses as soon as I have more than one generic type or more than two bounds per type. It separates the "what this function takes" from the "what these types must be able to do."
use std::fmt::Display;
// This is getting crowded and hard to read:
fn compare_and_print<T: PartialOrd + Display, U: PartialOrd + Display>(a: T, b: U) {
// ... logic ...
}
// This is much cleaner:
fn compare_and_print_clean<T, U>(a: T, b: U)
where
T: PartialOrd + Display,
U: PartialOrd + Display
{
// The logic stays the same, but the signature is breathable
println!("Comparing values...");
}📋 Practical Task
Exercise: Building a Generic High-Score Tracker
You are building a gaming leaderboard. You need to create a function called get_top_score that takes a slice of scores and returns the highest one. However, since scores could be integers (points) or floats (timed runs), the function must be generic.
Requirements:
- Implement a function
get_top_score<T>(scores: &[T]) -> Option<T>. - The function must use trait bounds to ensure
TimplementsPartialOrd(so you can compare them) andCopy(so you can return the value from the slice). - Use a
whereclause to define these bounds to keep the signature clean. - In your
mainfunction, test it with aVec<i32>and aVec<f64>.
There are no comments for now.