09. Traits
📋 Jump to Takeaways🎁 Two completely unrelated types, a tweet and a news article, and you want to call .summarize() on both. No shared parent class, no inheritance. How?
Traits are Rust's answer to interfaces, and they go further. A trait describes what a type can do, and any type can opt in. That gives you polymorphism without inheritance and, as you'll see, zero runtime cost.
Defining a Trait
How do you write code for a type you haven't even seen yet? You describe what it can do, not what it is. That's a trait: a contract. Any type that signs it gains that capability.
trait Summary {
fn summarize(&self) -> String;
}Any type that implements Summary now owes you a summarize method returning a String. The trait doesn't know or care what's inside the type. It only cares that the method exists.
Implementing Traits
A trait on its own does nothing. You bring it to life by implementing it for a concrete type with an impl Trait for Type block.
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
title: String,
author: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
fn main() {
let post = Article {
title: String::from("Rust is Fast"),
author: String::from("Alice"),
};
println!("{}", post.summarize()); // Rust is Fast by Alice
}Each type brings its own logic. Implement Summary for ten different structs and you get ten different summaries, all callable the same way.
Default Methods
What if most types would write the same method the same way? Give the trait a default. Types use it as-is or override it.
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// Uses default summarize()
}
fn main() {
let tweet = Tweet {
username: String::from("rustlang"),
content: String::from("Exciting news!"),
};
println!("{}", tweet.summarize());
// (Read more from @rustlang...)
}Notice the default summarize calls summarize_author, which has no default. So Tweet implements one tiny method and gets the bigger one for free.
Traits as Parameters
Here's the payoff. You can write one function that accepts any type implementing a trait, using &impl Trait.
trait Summary {
fn summarize(&self) -> String;
}
fn notify(item: &impl Summary) {
println!("Breaking: {}", item.summarize());
}Pass it an Article, a Tweet, anything that implements Summary. The compiler generates specialized code for each concrete type you use, so there's zero runtime cost.
Returning impl Trait
It works the other way too. You can return something that implements a trait without naming the concrete type.
trait Summary {
fn summarize(&self) -> String;
}
struct Article { title: String }
impl Summary for Article {
fn summarize(&self) -> String {
self.title.clone()
}
}
fn create_summary() -> impl Summary {
Article { title: String::from("New Discovery") }
}
fn main() {
let item = create_summary();
println!("{}", item.summarize()); // New Discovery
}Handy when the real return type is long and ugly, or when you want to hide it. The caller only knows it gets "something that implements Summary."
The Derive Macro
Implementing Debug or Clone by hand every time would be miserable. So Rust writes them for you with #[derive], generating the implementations at compile time.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone();
println!("{:?}", p1); // Point { x: 1.0, y: 2.0 }
println!("{}", p1 == p2); // true
}#[derive] works for traits with obvious implementations. You can derive Debug, Clone, Copy, PartialEq, Eq, Hash, Default, and more.
Common Standard Library Traits
You'll bump into these traits in almost every Rust program. Learn them once and the standard library stops feeling mysterious.
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
struct Color {
r: u8,
g: u8,
b: u8,
}
// Display must be implemented manually
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
fn main() {
let red = Color { r: 255, g: 0, b: 0 };
let default = Color::default();
println!("{}", red); // #ff0000 (Display)
println!("{:?}", red); // Color { r: 255, g: 0, b: 0 } (Debug)
println!("{:?}", default); // Color { r: 0, g: 0, b: 0 } (Default)
let copy = red; // Copy — no move!
println!("{} == {}: {}", red, copy, red == copy); // #ff0000 == #ff0000: true
}- Display, user-facing formatting (
{}) - Debug, developer-facing formatting (
{:?}) - Clone, explicit deep copy (
.clone()) - Copy, implicit bitwise copy (small, stack-only types)
- PartialEq, equality comparison (
==,!=) - Default, provides a zero/empty value
Key Takeaways
- Traits define shared behavior, they're Rust's answer to interfaces
impl Trait for Typeprovides the concrete implementation- Default methods let a trait supply behavior with minimal required implementation
&impl Traitas a parameter accepts any type that implements the trait-> impl Traithides the concrete return type behind a trait contract#[derive]auto-generates common trait implementations at compile time- Display, Debug, Clone, Copy, PartialEq, Default are std traits you'll reach for constantly
🎁 Traits say what a type can do. But what if you want your own Pair struct to hold two ints, or two strings, from a single definition, and pay nothing at runtime for that flexibility? Next up: generics, and the trait bounds that keep them safe.