Traits
Traits define shared behavior. They’re similar to interfaces in other languages, but with default implementations, zero-cost static dispatch, and powerful composition.
Defining and Implementing Traits
trait Summary {
fn summarize(&self) -> String;
// Default implementation - implementors can override or use as-is
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..20])
}
}
struct Article {
title: String,
body: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, &self.body[..50])
}
// preview() uses the default implementation
}
Trait Bounds
Constrain generic types to require certain behavior.
// Verbose form
fn notify<T: Summary>(item: &T) {
println!("Breaking: {}", item.summarize());
}
// Shorthand with impl Trait (preferred for simple cases)
fn notify(item: &impl Summary) {
println!("Breaking: {}", item.summarize());
}
// Multiple bounds
fn process<T: Summary + Clone>(item: &T) {
let copy = item.clone();
println!("{}", copy.summarize());
}
// Where clause for complex bounds
fn compare<T, U>(t: &T, u: &U) -> String
where
T: Summary + Clone,
U: Summary + std::fmt::Debug,
{
format!("{} vs {:?}", t.summarize(), u)
}
Static vs Dynamic Dispatch
Rust gives you a choice between compile-time and runtime polymorphism.
impl Trait - static dispatch (monomorphization)
The compiler generates a separate version of the function for each concrete type. Zero overhead at runtime.
fn print_summary(item: &impl Summary) {
println!("{}", item.summarize());
}
// Compiler generates:
// fn print_summary_article(item: &Article) { ... }
// fn print_summary_tweet(item: &Tweet) { ... }
dyn Trait - dynamic dispatch (trait objects)
Uses a vtable for runtime polymorphism. Small overhead, but allows heterogeneous collections.
fn print_summary(item: &dyn Summary) {
println!("{}", item.summarize());
}
// Store different types in one collection
let items: Vec<Box<dyn Summary>> = vec![
Box::new(article),
Box::new(tweet),
];
| Approach | When to use |
|---|---|
impl Trait | Default choice. Faster, optimized per type |
dyn Trait | Heterogeneous collections, plugin systems, reducing compile times |
Gotcha: Not all traits can be made into trait objects. The trait must be “object safe” - no
Selfin return types, no generic methods. The compiler tells you when this is violated.
Common Standard Library Traits
Derive macros - automatic implementations
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p); // Debug
let p2 = p.clone(); // Clone
assert_eq!(p, p2); // PartialEq
Essential traits reference
| Trait | Purpose | Derivable? |
|---|---|---|
Debug | Format with {:?} for logging/debugging | Yes |
Clone | Explicit deep copy via .clone() | Yes |
Copy | Implicit copy on assignment (stack types only) | Yes |
PartialEq / Eq | Equality comparison (==) | Yes |
PartialOrd / Ord | Ordering comparison (<, >, sort) | Yes |
Hash | Hashing for HashMap/HashSet keys | Yes |
Default | Provide a default value | Yes |
Display | Format with {} for user-facing output | No |
From / Into | Type conversion | No |
Iterator | Produce a sequence of values | No |
Display - user-facing formatting
use std::fmt;
struct Color(u8, u8, u8);
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
}
}
println!("{}", Color(255, 128, 0)); // "#ff8000"
From / Into - type conversion
Implementing From gives you Into for free.
struct Meters(f64);
impl From<f64> for Meters {
fn from(value: f64) -> Self {
Meters(value)
}
}
let m: Meters = 5.0.into(); // Into (free from From impl)
let m = Meters::from(5.0); // From (explicit)
Tip: Prefer implementing
FromoverInto. You get both conversions, andFromis more idiomatic. The?operator usesFromfor error conversion.
Iterator
struct Counter {
current: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
let sum: u32 = Counter { current: 0, max: 5 }
.filter(|n| n % 2 == 0)
.sum();
// sum = 2 + 4 = 6
Supertraits
Require another trait as a prerequisite.
trait Printable: std::fmt::Display {
fn print(&self) {
println!("{self}"); // can use Display because it's required
}
}
Extension Traits
Add methods to types you don’t own by defining a new trait.
trait StrExt {
fn is_blank(&self) -> bool;
}
impl StrExt for str {
fn is_blank(&self) -> bool {
self.trim().is_empty()
}
}
" ".is_blank(); // true
Tip: The caller must
usethe trait to access extension methods. This prevents conflicts between unrelated extensions.
Next: Error Handling | Ownership