Advanced Traits ​
Updated Mar 2026Beyond basic trait implementation: associated types vs generics, default type parameters, operator overloading, fully qualified syntax, supertraits, and the newtype pattern.
Associated Types vs Generic Parameters ​
Both let you abstract over types, but they work differently:
// ASSOCIATED TYPE: one implementation per type
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
// GENERIC PARAMETER: multiple implementations per type
pub trait ConvertTo<T> {
fn convert(&self) -> T;
}When to Use Which ​
| Feature | Associated Types | Generics |
|---|---|---|
| Implementations per type | Exactly one | Multiple |
| Caller annotation needed | No | Sometimes |
| Type inference | Better | Requires hints |
| Use case | Core behavior (Iterator) | Multiple conversions (From/Into) |
// Associated type: u32 can only iterate one way
impl Iterator for Counter {
type Item = u32; // fixed: Counter always yields u32
fn next(&mut self) -> Option<u32> { /* ... */ }
}
// Generic: u32 can convert to multiple types
impl ConvertTo<f64> for u32 {
fn convert(&self) -> f64 { *self as f64 }
}
impl ConvertTo<String> for u32 {
fn convert(&self) -> String { self.to_string() }
}Default Generic Type Parameters ​
Provide a default type so users don't have to specify it:
// std::ops::Add definition:
pub trait Add<Rhs = Self> { // default: Rhs is Self
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}This means you can implement Add without specifying Rhs:
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
// Rhs defaults to Point (Self)
impl std::ops::Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
let p = Point { x: 1, y: 0 } + Point { x: 2, y: 3 };
assert_eq!(p, Point { x: 3, y: 3 });Operator Overloading ​
Rust supports overloading operators by implementing traits from std::ops:
| Operator | Trait | Method |
|---|---|---|
+ | Add<Rhs> | add(self, rhs) |
- | Sub<Rhs> | sub(self, rhs) |
* | Mul<Rhs> | mul(self, rhs) |
/ | Div<Rhs> | div(self, rhs) |
% | Rem<Rhs> | rem(self, rhs) |
- (unary) | Neg | neg(self) |
== | PartialEq | eq(&self, other) |
< > | PartialOrd | partial_cmp(&self, other) |
[] | Index<Idx> | index(&self, index) |
*x | Deref | deref(&self) |
Cross-Type Addition ​
Override the default Rhs to add different types:
struct Millimeters(u32);
struct Meters(u32);
impl std::ops::Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
let total = Millimeters(500) + Meters(2);
// total = Millimeters(2500)Fully Qualified Syntax for Disambiguation ​
When a type implements multiple traits with same-named methods:
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) { println!("This is your captain speaking."); }
}
impl Wizard for Human {
fn fly(&self) { println!("Up!"); }
}
impl Human {
fn fly(&self) { println!("*waving arms furiously*"); }
}Calling Specific Implementations ​
let person = Human;
// Call the inherent method
person.fly(); // *waving arms furiously*
// Call trait methods explicitly
Pilot::fly(&person); // This is your captain speaking.
Wizard::fly(&person); // Up!Fully Qualified Syntax for Associated Functions ​
For associated functions (no self parameter), use angle bracket syntax:
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String { String::from("Spot") }
}
impl Animal for Dog {
fn baby_name() -> String { String::from("puppy") }
}
// Dog::baby_name() calls the inherent method
println!("{}", Dog::baby_name()); // Spot
// <Dog as Animal>::baby_name() calls the trait method
println!("{}", <Dog as Animal>::baby_name()); // puppyGeneral form: <Type as Trait>::function(receiver_if_method, args...)
Supertraits ​
A trait can require another trait as a prerequisite:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string(); // requires Display
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("* {} *", output);
println!("{}", "*".repeat(len + 4));
}
}
// Must implement Display to implement OutlinePrint
struct Point { x: i32, y: i32 }
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl OutlinePrint for Point {}
// Now Point has outline_print() availableChained Supertraits ​
trait A {}
trait B: A {} // B requires A
trait C: A + B {} // C requires both A and BThe Newtype Pattern ​
Circumvents the orphan rule by wrapping an external type in a local struct:
// Can't impl Display for Vec<String> directly (orphan rule)
// But we CAN wrap it:
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("{}", w); // [hello, world]Zero-Cost Abstraction ​
The newtype wrapper is erased at compile time -- there is no runtime overhead. The compiled code is identical to using the inner type directly.
Implementing Deref for Ergonomics ​
use std::ops::Deref;
struct Wrapper(Vec<String>);
impl Deref for Wrapper {
type Target = Vec<String>;
fn deref(&self) -> &Vec<String> {
&self.0
}
}
let w = Wrapper(vec![String::from("hello")]);
println!("Length: {}", w.len()); // Deref coercion: calls Vec::lenNewtype Use Cases ​
| Use Case | Example |
|---|---|
| Implement external traits on external types | Display for Vec<T> |
| Type safety without runtime cost | Meters(f64) vs Seconds(f64) |
| Hiding implementation details | Public API uses wrapper, internal uses inner |
| Enforcing invariants | NonEmptyVec<T> that guarantees at least one element |
Trait Objects and Dynamic Dispatch ​
// Static dispatch: compiler generates specialized code (monomorphization)
fn notify(item: &impl Summary) {
println!("{}", item.summarize());
}
// Dynamic dispatch: vtable lookup at runtime
fn notify_dyn(item: &dyn Summary) {
println!("{}", item.summarize());
}
// Heterogeneous collection requires dynamic dispatch
let articles: Vec<Box<dyn Summary>> = vec![
Box::new(NewsArticle { /* ... */ }),
Box::new(Tweet { /* ... */ }),
];Object Safety Rules ​
A trait is object-safe (can be used as dyn Trait) only if:
- Return type is not
Self - No generic type parameters on methods
// Object-safe:
trait Draw {
fn draw(&self);
}
// NOT object-safe (returns Self):
trait Clone {
fn clone(&self) -> Self;
}
// NOT object-safe (generic method):
trait Serialize {
fn serialize<W: std::io::Write>(&self, writer: &mut W);
}Blanket Implementations ​
Implement a trait for all types that satisfy certain bounds:
// From std: any type implementing Display also gets ToString
impl<T: fmt::Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
// Now any Display type has .to_string()
let s = 42.to_string(); // works because i32: DisplayQuick Reference ​
| Pattern | Syntax | Use Case |
|---|---|---|
| Associated type | type Item; | One impl per type |
| Default type param | trait Add<Rhs=Self> | Sensible defaults |
| Operator overload | impl Add for T | Custom +, -, etc. |
| Fully qualified | <T as Trait>::method() | Disambiguation |
| Supertrait | trait A: B | Require prerequisite trait |
| Newtype | struct Wrap(Inner) | Orphan rule workaround |
| Blanket impl | impl<T: X> Y for T | Universal implementations |