16.3 Default Method Implementations
Now, here’s the part where traits get truly dangerous and you start to realize why they’re the backbone of Rust’s polymorphism. We’re about to give our traits bodies. That’s right, we can define default implementations for methods right inside the trait definition.
Think of it this way: a trait method is a promise. “Any type that implements this trait must provide this functionality.” A default implementation is you, the trait designer, being a generous god and saying, “Look, I know how 90% of you are going to fulfill this promise, so here’s the code. If you’re one of the special snowflakes, feel free to do your own thing, but the rest of you can just lean on this.”
Let’s resurrect our simple Greet trait and make it actually useful.
trait Greet {
fn greet(&self) -> String;
// A default method that uses the required one.
fn greet_loudly(&self) -> String {
format!("{}!!!", self.greet()).to_uppercase()
}
}
See what we did there? We provided a default for greet_loudly(). The beautiful part is that this default implementation calls the required method greet(). This is the classic template method pattern, and it’s ridiculously powerful. It means the default behavior can be defined in terms of the core, required behavior that each implementor must provide.
Now, when we implement this trait for a type, we only need to define greet(). The greet_loudly() method comes for free. We can use it immediately.
struct Person {
name: String,
}
impl Greet for Person {
fn greet(&self) -> String {
format!("Hello, my name is {}", self.name)
}
// No need to implement `greet_loudly`! We get the default.
}
fn main() {
let person = Person { name: "Alice".to_string() };
println!("{}", person.greet()); // "Hello, my name is Alice"
println!("{}", person.greet_loudly()); // "HELLO, MY NAME IS ALICE!!!"
}
But what if our type is, say, an ExcitedPerson who is always loud? We can override the default implementation, just like you’d override a method in a class-based language.
struct ExcitedPerson {
name: String,
}
impl Greet for ExcitedPerson {
fn greet(&self) -> String {
format!("Hi! I'm {}!", self.name)
}
// Override the default because we're... excited.
fn greet_loudly(&self) -> String {
self.greet() // Just use the regular greeting, but we're already excited!
}
}
fn main() {
let excited_person = ExcitedPerson { name: "Bob".to_string() };
println!("{}", excited_person.greet_loudly()); // "Hi! I'm Bob!"
}
The Golden Rule: Coherence and the self Receiver
Notice how the default method greet_loudly() uses self.greet(). This is only possible because the method takes &self (or &mut self, or self). The trait has no idea what concrete type self is, but it does know that whatever it is, it implements the Greet trait, and therefore has a greet() method. This is the magic sauce. You can’t just call any random function from a default method; it can only call other methods that are part of the same trait or are in scope for the trait. This keeps everything coherent.
When Defaults Are a Terrible Idea
Default methods are fantastic, but don’t get carried away. The biggest pitfall is adding a default method to an existing trait that’s already widely used. Why? Because it’s a breaking change. I’m serious.
If you publish a library with a trait MyTrait that has one method, foo(), and then later you add a new required method bar() without a default, you have just broken every single crate that implemented your trait. They now have to implement this new method they knew nothing about. This is a nightmare.
However, if you add a new method baz() with a default implementation, it’s a non-breaking change. All existing implementations automatically get the new method with its default behavior. This is why you’ll see a lot of library traits start sparse (only required methods) and then grow with default methods later. It’s a key strategy for maintaining backwards compatibility. The lesson: think carefully about your public API from the start. Required methods are a commitment; default methods are a helpful, and safely extensible, suggestion.
Defaults vs. No-Ops
Another common use for defaults is to provide sensible no-op implementations for methods an implementor might not care about. Imagine a trait for handling events:
trait EventHandler {
fn on_input(&self, _event: InputEvent) { /* Default: do nothing */ }
fn on_update(&self, _delta: f32) { /* Default: do nothing */ }
fn on_draw(&self) { /* Default: do nothing */ }
}
A struct can then implement EventHandler and only override the methods it’s actually interested in, leaving the others as quiet no-ops. It’s a much cleaner alternative to forcing users to implement twenty methods when they only need one. This pattern is everywhere in Rust, and it’s a testament to how defaults make traits flexible and ergonomic without sacrificing an ounce of control.