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
34: Default Trait Implementations
I've seen this happen in a lot of production codebases: a developer defines a trait, then spends the next three hours writing the exact same logic over and over again for every single struct that implements that trait. It's not a compiler error, but it's a "maintenance bug"—the kind of debt that bites you the moment you decide to change a single string or a logic gate in ten different places.
The Redundancy Trap
trait Logger {
fn log(&self, message: &str);
fn log_warning(&self, message: &str);
}
struct ConsoleLogger;
impl Logger for ConsoleLogger {
fn log(&self, message: &str) {
println!("[LOG]: {}", message);
}
fn log_warning(&self, message: &str) {
println!("[WARNING]: {}", message);
}
}
struct FileLogger;
impl Logger for FileLogger {
fn log(&self, message: &str) {
// Imagine complex file writing logic here
println!("Writing to file: {}", message);
}
fn log_warning(&self, message: &str) {
println!("[WARNING]: {}", message);
}
}
Take a look at log_warning. In both ConsoleLogger and FileLogger, it's doing the exact same thing: wrapping the message in a [WARNING] tag. If your boss comes to you tomorrow and says, "Actually, we need warnings to start with ⚠️ ALERT:", you now have to hunt down every single implementation of Logger in your project and change that string. That's a recipe for missing one and creating an inconsistent UI.
Centralizing Logic with Default Implementations
Rust lets you provide a default body for a method directly inside the trait definition. If a struct implements the trait but doesn't provide its own version of that method, Rust just uses the default. This effectively allows you to define "standard" behavior while still leaving the door open for customization.
Here is how we clean up that Logger mess:
trait Logger {
// This remains required because every logger writes differently
fn log(&self, message: &str);
// This is now a default implementation
fn log_warning(&self, message: &str) {
self.log(&format!("[WARNING]: {}", message));
}
}
struct ConsoleLogger;
impl Logger for ConsoleLogger {
fn log(&self, message: &str) {
println!("[LOG]: {}", message);
}
// No need to implement log_warning here!
}
struct FileLogger;
impl Logger for FileLogger {
fn log(&self, message: &str) {
println!("Writing to file: {}", message);
}
// No need to implement log_warning here either!
}
Notice what I did in the default implementation: I called self.log(). This is a powerful pattern. The default method handles the "formatting" logic, but it delegates the actual "writing" (the part that changes between structs) to the method that must be implemented. Now, if you want to change the warning prefix, you change it in one line, and every logger in your system is updated instantly.
Overriding the Default for Special Cases
Now, you might be thinking, "What if one specific logger needs to do something totally different for warnings?" Maybe your SmsLogger needs to send a high-priority flag to an API instead of just adding a string prefix.
The beauty of default implementations is that they are optional. You can simply override them in the impl block:
struct SmsLogger;
impl Logger for SmsLogger {
fn log(&self, message: &str) {
println!("Sending SMS: {}", message);
}
// We don't like the default [WARNING] prefix for SMS,
// so we provide our own specific logic.
fn log_warning(&self, message: &str) {
println!("Sending HIGH-PRIORITY SMS: {}", message);
}
}
By doing this, SmsLogger ignores the default logic and uses its own, while ConsoleLogger and FileLogger continue to use the shared logic. You've essentially created a "sane default" that stays out of your way until you actually need it.
📋 Practical Task
Building a Customizable Game Character Ability System
You are building a combat system for an RPG. Every character has a Character trait. All characters must implement a basic attack method, but you want most characters to share a default taunt method that prints "I'm going to win!". However, some characters should have unique taunts.
Your Task:
- Define a trait
Characterwith a required methodattack(&self)and a default methodtaunt(&self). - Create a struct
Warriorthat implementsCharacterusing the default taunt. - Create a struct
Roguethat implementsCharacterbut overrides the default taunt with something more stealthy (e.g., "You won't even see me coming..."). - In your
mainfunction, create instances of both and call bothattack()andtaunt()on each to verify that the Warrior uses the default and the Rogue uses the override.
There are no comments for now.