Traits are Rust's mechanism for polymorphism and code reuse; the whole standard library is built on them.
A trait declares a set of methods that types can implement, similar to an interface. You write trait Summary { fn summarize(&self) -> String; } and then impl Summary for Article { ... } for each type.
Traits can provide default methods that implementors get for free and may override. A default method can call the required ones, so a trait can ask for one small method and offer many on top. This is exactly how Iterator works: implement next, and you get map, filter, sum and dozens more.
To accept "anything that implements Summary", the simplest syntax is fn notify(item: &impl Summary). In return position, fn make() -> impl Summary returns some concrete type the caller cannot name. Both are resolved at compile time, with no runtime cost.
The orphan rule says you can implement a trait for a type only if the trait or the type is defined in your crate. You can implement your trait for Vec<T>, or Display for your struct, but not Display for Vec<T>. The usual workaround is a newtype wrapper.
Traits can require other traits (supertraits): trait Pretty: std::fmt::Display { ... } means implementors must also implement Display, and default methods may use to_string().
To call a trait method, the trait must be in scope. If x.summarize() mysteriously fails with "method not found", you probably forgot use crate::Summary;. Many crates provide a prelude module for this reason.
Since Rust 1.75, traits can contain async fn and -> impl Trait methods too.
Example
use std::fmt::Display;
trait Summary {
fn author(&self) -> String;
fn summarize(&self) -> String { format!("(Read more from {}...)", self.author()) } // default
}
struct Tweet { user: String, text: String }
struct Article { by: String }
impl Summary for Tweet {
fn author(&self) -> String { format!("@{}", self.user) }
fn summarize(&self) -> String { format!("{}: {}", self.author(), self.text) } // override
}
impl Summary for Article {
fn author(&self) -> String { self.by.clone() }
}
fn notify(item: &impl Summary) { println!("Breaking! {}", item.summarize()); }
fn loud<T: Display>(x: T) -> impl Display { format!("{x}!") }
fn main() {
notify(&Tweet { user: "rustlang".into(), text: "1.88 is out".into() }); // Breaking! @rustlang: 1.88 is out
notify(&Article { by: "Ferris".into() }); // Breaking! (Read more from Ferris...)
println!("{}", loud(42)); // 42!
}Remember
- A trait lists required methods and may supply default ones
&impl Traitparameters and-> impl Traitreturns are static and zero-cost- Orphan rule: you own the trait or the type; bring traits into scope to call their methods
Watch out for
- Forgetting to
usea trait and wondering why its method does not exist.