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
33: Defining and Implementing Traits
I was working on a small payment module the other day, and I ran into a problem that perfectly illustrates why we need traits. I had a CreditCard struct and a PayPal struct. Both had a method called process_payment, but they were completely different types. I wanted to write a single function that could take any payment method and just run the process.
The Wall I Hit With Generics
My first instinct was to use a generic. I figured, "I'll just tell Rust the function takes some type T, and I'll call process_payment on it." Here is what I tried:
struct CreditCard { number: String }
struct PayPal { email: String }
impl CreditCard {
fn process_payment(&self, amount: f64) {
println!("Charging ${} to card {}", amount, self.number);
}
}
impl PayPal {
fn process_payment(&self, amount: f64) {
println!("Charging ${} to PayPal account {}", amount, self.email);
}
}
fn execute_payment<T>(method: T, amount: f64) {
method.process_payment(amount);
}
I hit cargo build and the compiler immediately yelled at me. It basically said: "I have no idea what T is. For all I know, T could be an integer or a boolean, and neither of those have a process_payment method."
Even though I knew I'd only ever pass in a CreditCard or PayPal, Rust requires absolute certainty. I can't just assume a method exists; I have to prove it.
Defining the Contract
This is where traits come in. A trait is essentially a contract. Instead of telling Rust "this is some type T," I can tell it "this is some type T that implements the PaymentProcessor trait."
First, I need to define what that contract looks like. I don't write the logic here; I just define the signature of the methods that any "payment processor" must have:
trait PaymentProcessor {
fn process_payment(&self, amount: f64);
}
Now, my existing structs don't actually follow this contract yet. They just happen to have methods with the same name. I have to explicitly implement the trait for each one. Note that the syntax changes slightly—instead of impl CreditCard, it's impl PaymentProcessor for CreditCard.
impl PaymentProcessor for CreditCard {
fn process_payment(&self, amount: f64) {
println!("Charging ${} to card {}", amount, self.number);
}
}
impl PaymentProcessor for PayPal {
fn process_payment(&self, amount: f64) {
println!("Charging ${} to PayPal account {}", amount, self.email);
}
}
Tying It All Together
Now for the magic part. I can go back to my execute_payment function. But this time, I'm going to use a trait bound. I'll tell Rust that T must implement PaymentProcessor.
fn execute_payment<T: PaymentProcessor>(method: T, amount: f64) {
method.process_payment(amount);
}
Wait, I actually prefer a cleaner syntax when the bounds get long. You'll often see the where clause instead, which does the exact same thing but keeps the function signature from getting cluttered:
fn execute_payment<T>(method: T, amount: f64)
where
T: PaymentProcessor
{
method.process_payment(amount);
}
Now, if I try to pass a String into execute_payment, the compiler will stop me. But if I pass a CreditCard or PayPal, it works perfectly. I've effectively decoupled the execution logic from the specific payment implementation. If I decide to add CryptoPayment tomorrow, I don't have to touch the execute_payment function at all—I just implement the trait for the new struct.
📋 Practical Task
Implementing a VolumeCalculator for Geometric Shapes
You are building a 3D modeling tool. You need to calculate the total volume of a scene containing various shapes.
- Define a trait called
VolumeCalculatorwith a single methodcalculate_volume(&self) -> f64. - Create two structs:
Cube(with asidefield) andSphere(with aradiusfield). - Implement the
VolumeCalculatortrait for both structs.- Hint: Volume of a cube is side³, volume of a sphere is (4/3) * π * radius³. You can use
std::f64::consts::PI.
- Hint: Volume of a cube is side³, volume of a sphere is (4/3) * π * radius³. You can use
- Write a generic function called
print_volume<T: VolumeCalculator>(shape: T)that calls thecalculate_volumemethod and prints the result to the console. - In your
mainfunction, instantiate one cube and one sphere, and pass both toprint_volume.
There are no comments for now.