Advanced Traits
Prerequisites: Level 2 — Intermediate
Level 2 used traits to share behaviour and bound generics. This lesson covers the parts of the trait system you meet once you design libraries: associated types, trait objects and their limits, supertraits, operator overloading, blanket implementations and the orphan rule.
Associated types
An associated type is a type that each implementation chooses once.
Iterator is the classic example: type Item; is fixed per iterator.
trait Container {
type Item;
fn get(&self, index: usize) -> Option<&Self::Item>;
}
struct Names(Vec<String>);
impl Container for Names {
type Item = String;
fn get(&self, index: usize) -> Option<&String> {
self.0.get(index)
}
}
Use an associated type when there is exactly one sensible choice per
implementing type. Use a generic parameter (trait From<T>) when a type
should implement the trait several times with different types.
Operator overloading
Operators are traits in std::ops. a + b calls Add::add(a, b), and the
right-hand side is a generic parameter (defaulting to Self), so you can
implement Mul<f64> alongside Mul<Vec2>:
use std::ops::{Add, Mul};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
x: f64,
y: f64,
}
impl Add for Vec2 {
type Output = Vec2;
fn add(self, rhs: Vec2) -> Vec2 {
Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
impl Mul<f64> for Vec2 {
type Output = Vec2;
fn mul(self, k: f64) -> Vec2 {
Vec2 { x: self.x * k, y: self.y * k }
}
}
fn main() {
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 0.5, y: -1.0 };
println!("{:?}", a + b);
println!("{:?}", a * 3.0);
println!("{}", a + b == Vec2 { x: 1.5, y: 1.0 });
}
Expected output:
Vec2 { x: 1.5, y: 1.0 }
Vec2 { x: 3.0, y: 6.0 }
true
Comparison operators come from PartialEq/PartialOrd, indexing from
Index/IndexMut, and += from AddAssign. Overload an operator only when
its meaning is obvious for your type.
Generics vs trait objects
There are two ways to accept "anything that implements Shape":
trait Shape {
fn area(&self) -> f64;
}
struct Square(f64);
struct Circle(f64);
impl Shape for Square {
fn area(&self) -> f64 { self.0 * self.0 }
}
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
}
// Static dispatch: one copy compiled per T, all elements the same type.
fn total_generic<T: Shape>(shapes: &[T]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
// Dynamic dispatch: one copy, calls go through a vtable, types can mix.
fn total_dyn(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let squares = [Square(1.0), Square(2.0)];
let mixed: Vec<Box<dyn Shape>> = vec![Box::new(Square(2.0)), Box::new(Circle(1.0))];
println!("{:.2}", total_generic(&squares));
println!("{:.2}", total_dyn(&mixed));
}
Expected output:
5.00
7.14
Generics are usually faster (calls can be inlined) but grow the binary.
dyn Trait is the tool for heterogeneous collections and plugin-style APIs.
Dyn compatibility
Not every trait can become dyn Trait. A trait is dyn compatible
(formerly "object safe") only if the compiler can build a vtable for it.
Roughly, its methods must not:
- return
Selfor takeSelfby value in a way that needs its size, - have their own generic type parameters,
- be associated functions without a
selfreceiver.
A method you want to exclude from the vtable can opt out with
where Self: Sized; it stays callable on concrete types.
Supertraits and blanket impls
A supertrait says "implementing this requires that too". A blanket impl
implements a trait for every type satisfying a bound — std uses one to give
every Display type a to_string().
use std::fmt::Display;
trait Labelled: Display {
fn label(&self) -> String {
format!("[{self}]") // allowed because Self: Display
}
}
// Every Display type is now Labelled.
impl<T: Display> Labelled for T {}
fn main() {
println!("{} {}", 42.label(), "ok".label());
}
Expected output:
[42] [ok]
The orphan rule and the newtype pattern
You may implement a trait for a type only if the trait or the type is defined in your crate. This keeps two crates from supplying conflicting impls. To implement a foreign trait for a foreign type, wrap it:
use std::fmt;
struct Csv(Vec<String>);
impl fmt::Display for Csv {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.join(","))
}
}
Csv(vec!["id".into(), "name".into()]) now displays as id,name. The
wrapper costs nothing at runtime. Add Deref or explicit forwarding
methods only if callers genuinely need the inner API.
Common mistakes
- Implementing a foreign trait on a foreign type directly:
impl std::fmt::Display for Vec<String> { /* ... */ }
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
- Making a trait with
fn duplicate(&self) -> Self;and then usingBox<dyn ThatTrait>. The compiler reportserror[E0038]because the trait is not dyn compatible. Addwhere Self: Sizedto that method, or returnBox<dyn ThatTrait>instead. - Reaching for
dyn Traitby default. If every element has the same type, a generic is simpler and faster.
Practice
Define a Money newtype around i64 cents. Implement Add (returning
Money), Display (formatting as $12.34), and std::iter::Sum so
iter().copied().sum::<Money>() works. Print the total of [1250, 99, 1].
Show solution
use std::fmt;
use std::iter::Sum;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Money(i64);
impl Add for Money {
type Output = Money;
fn add(self, rhs: Money) -> Money {
Money(self.0 + rhs.0)
}
}
impl Sum for Money {
fn sum<I: Iterator<Item = Money>>(iter: I) -> Money {
iter.fold(Money(0), |acc, m| acc + m)
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "${}.{:02}", self.0 / 100, self.0 % 100)
}
}
fn main() {
let items = [Money(1250), Money(99), Money(1)];
let total: Money = items.iter().copied().sum();
println!("{total}");
}
This prints $13.50. (Negative amounts would need extra handling in
Display.)