In every language, you need a way to say "these different types all share this behavior." Ruby calls them modules. Java calls them interfaces. Go calls them interfaces. Rust calls them traits.
Traits are Rust's cornerstone of polymorphism. They let you write code that works with any type — as long as that type implements the trait.
Defining a Trait
A trait defines a set of methods:
trait Summary {
fn summarize(&self) -> String;
fn author(&self) -> &str {
"Anonymous" // default implementation
}
}
Two parts:
summarize(&self) -> String— required method (no body)author(&self) -> &str— provided method (has default body)
Types implement the trait with impl Trait for Type:
struct Article {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
fn author(&self) -> &str {
&self.author
}
}
struct Tweet {
username: String,
content: String,
reply: bool,
retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.content)
}
}
Now both Article and Tweet can be used wherever Summary is expected.
Using Traits
You can use traits as parameter types:
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
This function accepts any reference to a type that implements Summary.
Or with trait bounds (more explicit):
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
The T: Summary syntax means "any type T that implements Summary."
Multiple Trait Bounds
What if you need multiple traits?
fn notify(item: &(impl Summary + Display)) {
println!("{}", item);
}
Or the cleaner form:
fn notify<T: Summary + Display>(item: &T) {
println!("{}", item);
}
Returning Trait Types
You can return types that implement a trait:
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
This is useful for closures and iterators too.
Trait Bounds with where
When things get complex, where clauses help readability:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
Instead of cramming bounds in the angle brackets, you list them cleanly below.
Default Implementations
Provided methods can call other methods — even ones not yet implemented:
trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
fn summarize_author(&self) -> String {
format!("(Read more from {})", self.author())
}
fn author(&self) -> &str; // required
}
The summarize_author method uses author() even though it's not defined here — the implementing type must provide it.
Trait Objects
What if you need a collection of different types? Trait objects enable runtime polymorphism:
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
}
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
println!("Total area: {}", total_area(&shapes));
}
dyn Trait is the keyword. Box<dyn Shape> means "a pointer to any type implementing Shape."
Trade-off: Trait objects use dynamic dispatch (runtime lookup). Generic functions use static dispatch (compile-time). The former is flexible; the latter is faster.
The Standard Library Traits
Rust provides important traits you'll use constantly:
Display— formatting for user output ({})Debug— debugging format ({:?})Clone— deep copyCopy— bitwise copy (no resources)Default— default valuePartialEq,Eq— equality comparisonPartialOrd,Ord— orderingIterator— iteration (you'll implement this a lot)
Deriving most of these is automatic:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct Point {
x: f64,
y: f64,
}
The Big Picture
Traits are how Rust achieves polymorphism without inheritance. Instead of a type hierarchy ("a Tweet is a kind of Summary"), you have implementations ("Tweet implements Summary").
This is more flexible. A type can implement multiple traits. Traits can be added to existing types (even types you didn't write) via the orphan rule or newtypes.
It's also more explicit. When you see &impl Summary, you know exactly what that function can do — just what's in the trait.
Next up: Chapter 11 — Generics. Writing code once, using it for many types.