Traits in Rust
Traits define shared behavior. Types implement traits to opt into that behavior. Use trait bounds to write generic code over behaviors.
Declaring and implementing traits
trait Area {
fn area(&self) -> f64; // required method
fn describe(&self) -> String { // default method
String::from("shape")
}
}
struct Circle { r: f64 }
impl Area for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}
Trait bounds on functions
fn total_area<T: Area>(shapes: &[T]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
// where-clause for readability
fn print_all<T>(xs: &[T])
where
T: std::fmt::Display,
{
for x in xs { println!("{x}"); }
}
Use impl Trait in argument/return position for simpler signatures:
fn first(xs: impl IntoIterator<Item = i32>) -> Option<i32> { xs.into_iter().next() }
Associated types
Prefer associated types for relationships inside traits.
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Trait objects (dyn Trait)
Use &dyn Trait or Box<dyn Trait> for dynamic dispatch when you need heterogenous collections.
fn draw_all(xs: &[&dyn Area]) { for x in xs { println!("{}", x.describe()); } }
Object safety rules limit which traits can be made into trait objects (no generic methods, Self in wrong places).
Blanket impls and coherence
The orphan rule prevents conflicting impls: you can only implement a trait for a type if either the trait or the type is local to your crate.
Supertraits and auto traits
trait Printable: std::fmt::Display {}
Send/Sync are auto traits derived from field types.
Derive and standard traits
Derive common traits for convenience: Debug, Clone, Copy, Eq, PartialEq, Ord, Hash, Default.
Summary
- Traits define behavior; impl them for your types
- Use bounds/
impl Traitto write generic code; associated types model type families - Use
dyn Traitfor dynamic dispatch; mind object-safety and coherence