A trait is a contract that defines behaviors other types can implement.
If you come from Java or C#, it’s conceptually similar to an Interface. If you come from C++, it’s similar to an Abstract Class (with pure virtual functions). If you come from Haskell, it’s a Type Class.
In essence, a Trait defines shared functionality. It tells the compiler: “I don’t care what this object is, I only care that it has a method called x”.
Defining a Trait
Imagine we’re building a news application. We have Article, Tweet, and FacebookStatus. We want a unified way to ask for a “summary” of each of them.
We define a trait using the trait keyword:
pub trait Summarizable {
// We declare the method signature, but not the body (usually)
fn summarize(&self) -> String;
}Any type that wants to be Summarizable must implement this method with that exact signature.
Implementing a Trait on a Type
Now let’s implement this behavior for two different structs. The syntax is:
impl TraitName for TypeName.
pub struct Article {
pub title: String,
pub author: String,
pub content: String,
}
impl Summarizable for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
}
impl Summarizable for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}Now, even though Article and Tweet are completely different types, they both share a behavior.
fn main() {
let tweet = Tweet {
username: String::from("rust_lang"),
content: String::from("Traits are awesome!"),
};
println!("New tweet: {}", tweet.summarize());
}Default Implementations
Unlike old Java interfaces, Traits in Rust can have code. We can provide a default behavior so types don’t have to implement it mandatorily if they don’t want to.
pub trait Summarizable {
// Default implementation
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}If we now create a struct and leave the impl block empty, it will use the default:
struct Photo {
url: String,
}
impl Summarizable for Photo {} // Empty block = use default
fn main() {
let p = Photo { url: String::from("img.jpg") };
println!("{}", p.summarize()); // Prints: (Read more...)
}Default implementations can call other methods of the same trait, even if those other methods don’t have default implementations. This allows creating “template methods”.
Traits as Parameters
Traits combine with functions to give us polymorphism.
We can write a function that accepts anything that implements the Summarizable trait.
// "impl Trait" syntax (Syntactic sugar)
pub fn notify(item: &impl Summarizable) {
println!("Breaking news: {}", item.summarize());
}
fn main() {
let article = Article { ... };
let tweet = Tweet { ... };
notify(&article); // ✅ Works
notify(&tweet); // ✅ Works
}The notify function doesn’t know if you’re passing it a Tweet or an Article. It only knows it can call .summarize().
The Orphan Rule
There’s an important restriction in Rust on where you can implement a trait. To implement a trait on a type, at least one of the two must be local to your Crate (your project).
- ✅ You can implement your
Summarizabletrait on yourTweettype. - ✅ You can implement your
Summarizabletrait on the external typeString(from the standard library). - ✅ You can implement the external trait
Display(from std) on yourTweettype. - ❌ You CANNOT implement the external trait
Displayon the external typeString.
// ❌ Error: Impl of external trait for external type
impl std::fmt::Display for String { ... }This rule prevents incompatible implementations. Without it, two libraries could define different ways to print a String and there would be no consistent choice when both participate in the same program.