12. 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 at zero runtime cost (the compiler generates a separate copy of the function for each concrete type, so there's no indirection at runtime).
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
}You can't move a field out of a borrowed &self, so .clone() creates a new owned copy to return.
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) // :02x = 2-digit lowercase hex, zero-padded
}
}
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
}The write! macro writes formatted text into the buffer f that Rust provides, similar to how Go's fmt.Fprintf writes to a writer instead of returning a string.
- Display, user-facing formatting (
{}) - Debug, developer-facing formatting (
{:?}) - Clone, explicit deep copy (
.clone()) - Copy, implicit bitwise copy (small, stack-only types). A type can only be
Copyif all its fields areCopy. Anything containing aString,Vec, or heap data can't be. - PartialEq, equality comparison (
==,!=) - Default, provides a zero/empty value (like Go's automatic zero values, but opt-in)
Default in Practice
In Go, Config{} gives you zero values automatically. In Rust, you derive Default and call .default():
#[derive(Debug, Default)]
struct Config {
port: u16, // default: 0
debug: bool, // default: false
name: String, // default: ""
}
fn main() {
let c = Config::default();
println!("{:?}", c); // Config { port: 0, debug: false, name: "" }
// Override specific fields, default the rest
let c2 = Config {
port: 8080,
..Default::default()
};
println!("{:?}", c2); // Config { port: 8080, debug: false, name: "" }
}..Default::default() fills remaining fields with their defaults. It's Rust's equivalent of Go's partial struct literal (Config{Port: 8080} where unset fields are zero).
PartialEq vs Eq
PartialEq and Eq both let you use ==, but they promise different things.
PartialEq— gives you==and!=. It only requires that you implement aneq()method. It does NOT promise thata == ais true for every value.Eq— a marker trait (no extra methods). It extendsPartialEqand adds one guarantee: reflexivity, meaninga == ais alwaystrue, no exceptions.
Why would a == a ever be false? Floats.
#[derive(Debug, PartialEq)]
struct Temperature {
celsius: f64,
}
fn main() {
let freezing = Temperature { celsius: 0.0 };
let boiling = Temperature { celsius: 100.0 };
println!("{}", freezing == boiling); // false
let nan = f64::NAN;
println!("{}", nan == nan); // false — breaks reflexivity, so f64 can't derive Eq
}f64 implements PartialEq but not Eq, because NaN != NaN. That's why Temperature above can only derive PartialEq, adding Eq to the derive list would fail to compile since one of its fields (f64) doesn't implement Eq.
Types built from comparable fields (integers, strings, bools) can derive both:
#[derive(Debug, PartialEq, Eq)]
struct Point {
x: i32,
y: i32,
}This matters in practice because HashMap and HashSet require their key type to implement Eq (plus Hash), since their correctness depends on that reflexivity guarantee. That's part of why you can't use f64 directly as a HashMap key.
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
PartialEqvsEq,Eqpromisesa == ais always true (reflexivity); types withf64fields can't derive it
🎁 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.