16.2 Implementing a Trait for a Type
Right, so you’ve defined this beautiful, abstract trait. It’s a perfect little contract, a promise of behavior. Now comes the fun part: actually making one of your types keep that promise. This is where the rubber meets the road and where you’ll spend most of your time. Implementing a trait for a type is the act of saying, “Yes, my Thingamajig can absolutely do the stuff the Doable trait requires, and here’s exactly how it does it.”
The syntax is straightforward, almost deceptively so. You use the impl keyword, but instead of it being for the type by itself (impl MyType), you’re implementing a trait for a type (impl MyTrait for MyType). Everything you write inside this block must fulfill the trait’s contract. No ifs, ands, or buts. The compiler is a ruthless lawyer on this point, and it will not let you compile code that makes a promise it can’t keep.
Let’s take a ridiculously simple trait.
trait Greet {
fn greet(&self) -> String;
}
Now, let’s implement it for a standard library type, just to prove we can. This is a powerful pattern for extending the behavior of types you don’t even own.
impl Greet for i32 {
fn greet(&self) -> String {
format!("Hello, I am the number {}", self)
}
}
fn main() {
let number = 42;
println!("{}", number.greet()); // Prints: "Hello, I am the number 42"
}
See? Now every integer has existential dread and introduces itself politely. The trait method is called just like any other method, using the dot notation (number.greet()). The trait’s name is brought into scope so the compiler knows what greet we’re talking about, but the call site is beautifully clean.
The Almighty self
Pay close attention to the signature of the trait method you’re implementing. The &self (or &mut self, or self) is not a suggestion; it’s part of the contract. If the trait says &self, you’re getting an immutable reference to your type. You can read from it but not modify it. If it says &mut self, you’re getting a mutable reference, and you’re allowed to change things. If it says self (by value), you’re consuming the instance. Getting this wrong is a classic rookie mistake. The compiler will, of course, catch it, but it’s a sign you might not have thought through what the trait method is supposed to do.
Implementing for Your Own Types
This is the more common scenario. You have a struct or enum you built, and you want it to have some shared behavior.
struct Dog {
name: String,
age: u8,
}
impl Greet for Dog {
fn greet(&self) -> String {
if self.age > 10 {
format!("*slow, lazy wag* Hi, I'm {}, an old doggo", self.name)
} else {
format!("BORK! I'm {}! BORK!", self.name)
}
}
}
Here, our implementation is specific to Dog. We can use the struct’s fields (self.name, self.age) to create the unique greeting behavior. This is the whole point: the what is defined by the trait (greet returns a String), but the how is defined by the type’s implementation.
Dealing with Associated Functions
Not all trait methods take self. Sometimes a trait will define an associated function (often mistakenly called a “static method”), which looks like a constructor.
trait DefaultGreeter {
fn default_greet() -> String; // No self parameter
}
impl DefaultGreeter for Dog {
fn default_greet() -> String {
String::from("BORK! I am a generic dog!")
}
}
// Called using :: syntax
let greeting = Dog::default_greet();
Notice how we implement this without any instance of a Dog. We’re implementing the function for the type itself, not for any particular value. It’s a namespaced function under Dog.
The Orphan Rule: Rust’s Guardrails
Here’s where we hit Rust’s famously restrictive “Orphan Rule.” It states that you can only implement a trait for a type if either the trait or the type is defined in your current crate. This prevents absolute chaos. Imagine if another crate could implement a trait you defined for a type you defined, and that implementation did something malicious or just plain stupid. Your crate’s behavior would change depending on what your users depended on! A nightmare. The Orphan Rule keeps trait implementations local and predictable. It’s a pain sometimes when you just want to implement Serialize for that type from another library, but it’s a pain that saves you from far worse architectural disasters. (The workaround is the “newtype pattern,” wrapping the external type in a local tuple struct, which we’ll cover elsewhere. The compiler giveth, and the compiler taketh away, but it usually gives a workaround.)