Traits Generics
Rust traits and generics for polymorphism, abstraction, and zero-cost generic programming
You are an expert in Rust traits and generics for building flexible, reusable abstractions with zero-cost polymorphism.
## Key Points
- Prefer static dispatch (generics) for performance-critical paths; use `dyn Trait` when you need heterogeneous collections or to reduce binary size.
- Use associated types for traits where each type has exactly one implementation (like `Iterator::Item`).
- Keep trait definitions small and focused — prefer composing multiple small traits over one large one.
- Derive common traits (`Debug`, `Clone`, `PartialEq`, `Eq`, `Hash`) whenever possible.
- Use `where` clauses for readability when bounds get complex.
- Implement `From` / `Into` for type conversions rather than ad-hoc methods.
- **Orphan rule**: You cannot implement a foreign trait on a foreign type. Use the newtype pattern as a workaround.
- **Monomorphization bloat**: Heavy use of generics can increase binary size since each instantiation generates separate code. Consider `dyn Trait` or extracting non-generic inner functions.
- **Overly broad bounds**: Adding too many trait bounds to generics makes APIs rigid. Only require what the function actually uses.
- **Forgetting `+ Send + Sync`**: Trait objects used across threads need explicit `dyn Trait + Send + Sync` bounds.
## Quick Example
```rust
trait PrettyPrint: std::fmt::Display {
fn pretty(&self) -> String {
format!("[ {} ]", self)
}
}
```skilldb get rust-skills/Traits GenericsFull skill: 217 linesTraits & Generics — Rust Programming
You are an expert in Rust traits and generics for building flexible, reusable abstractions with zero-cost polymorphism.
Core Philosophy
Traits and generics are Rust's answer to polymorphism, and their design reflects a strong opinion: abstraction should not cost performance. Generics are monomorphized at compile time, producing specialized machine code for each concrete type. This means you get the flexibility of interfaces with the speed of hand-written specialized code. The trade-off is compile time and binary size, but at runtime there is zero indirection.
The trait system encodes behavior contracts in the type system. When a function requires T: Display + Clone, it documents exactly what capabilities it needs -- no more, no less. This is more precise than class hierarchies because traits compose orthogonally. A type can implement any combination of traits without fitting into a single inheritance tree, and each trait implementation is independently verifiable.
Dynamic dispatch via dyn Trait exists for the cases where you genuinely need heterogeneous collections or want to reduce code generation. The choice between impl Trait (static) and dyn Trait (dynamic) should be deliberate: static dispatch for hot paths and known types, dynamic dispatch for plugin systems, collections of mixed types, and reducing monomorphization bloat. Making this choice explicit -- rather than defaulting to virtual dispatch as in most OOP languages -- is a core part of Rust's performance-by-design philosophy.
Anti-Patterns
- Overly broad trait bounds: Adding
T: Clone + Debug + Send + Sync + 'staticto every generic function when onlyCloneis actually used makes APIs rigid and hard to call. Require only the bounds the function body actually exercises. - Reaching for
dyn Traitby default: Coming from OOP languages, it is tempting to use trait objects everywhere. Butdyn Traitincurs heap allocation and vtable indirection. Prefer generics for known-type scenarios and reservedyn Traitfor genuinely heterogeneous collections. - Defining one large trait instead of several small ones: A trait with 15 methods forces every implementor to provide all of them. Split into focused traits (
Read,Write,Seek) that implementors can adopt independently, and use supertraits to compose them when needed. - Implementing foreign traits on foreign types without newtype: The orphan rule exists to prevent conflicting implementations across crates. Attempting to work around it with blanket impls or feature flags leads to fragile, incompatible code. Use the newtype pattern to wrap foreign types cleanly.
- Ignoring monomorphization bloat in generic-heavy libraries: Each unique instantiation of a generic function generates separate machine code. A generic function called with 20 different types produces 20 copies. For large generic functions, extract the non-generic core into a helper that operates on
dyn Traitor concrete types to reduce binary size.
Overview
Traits define shared behavior (like interfaces) and generics allow writing code that works across multiple types. Together they form Rust's primary abstraction mechanism. Rust monomorphizes generic code at compile time, producing specialized machine code with no runtime overhead.
Core Concepts
Defining and Implementing Traits
trait Summary {
fn summarize(&self) -> String;
// Default implementation
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..20.min(self.summarize().len())])
}
}
struct Article {
title: String,
body: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, &self.body[..50.min(self.body.len())])
}
}
Generic Functions with Trait Bounds
// Bound syntax
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// where clause (cleaner for multiple bounds)
fn process<T, U>(t: &T, u: &U) -> String
where
T: Summary + Clone,
U: std::fmt::Display,
{
format!("{} — {u}", t.summarize())
}
impl Trait (Argument Position and Return Position)
// Argument position: syntactic sugar for a generic bound
fn notify(item: &impl Summary) {
println!("Breaking: {}", item.summarize());
}
// Return position: returns some concrete type implementing the trait
fn make_summary() -> impl Summary {
Article {
title: "News".into(),
body: "Something happened today in the world.".into(),
}
}
Trait Objects (Dynamic Dispatch)
// dyn Trait behind a pointer for runtime polymorphism
fn print_all(items: &[&dyn Summary]) {
for item in items {
println!("{}", item.summarize());
}
}
// Boxed trait objects for owned, heap-allocated polymorphism
fn create_items() -> Vec<Box<dyn Summary>> {
vec![
Box::new(Article { title: "A".into(), body: "Body A content here...".into() }),
]
}
Supertraits
trait PrettyPrint: std::fmt::Display {
fn pretty(&self) -> String {
format!("[ {} ]", self)
}
}
Implementation Patterns
Generic Structs
struct Pair<T> {
first: T,
second: T,
}
impl<T: PartialOrd + std::fmt::Display> Pair<T> {
fn larger(&self) -> &T {
if self.first >= self.second { &self.first } else { &self.second }
}
}
Associated Types vs. Generic Parameters
Use associated types when there is one natural type per implementation:
trait Iterator {
type Item; // associated type — one Item per iterator
fn next(&mut self) -> Option<Self::Item>;
}
// vs. generic param — multiple implementations possible
trait ConvertTo<T> {
fn convert(&self) -> T;
}
Extension Traits
Add methods to foreign types via a new trait:
trait StringExt {
fn truncate_to(&self, max_len: usize) -> &str;
}
impl StringExt for str {
fn truncate_to(&self, max_len: usize) -> &str {
if self.len() <= max_len {
self
} else {
&self[..self.floor_char_boundary(max_len)]
}
}
}
Blanket Implementations
trait Greet {
fn greet(&self) -> String;
}
// Implement for anything that implements Display
impl<T: std::fmt::Display> Greet for T {
fn greet(&self) -> String {
format!("Hello, {self}!")
}
}
The Newtype Pattern
Implement foreign traits on foreign types by wrapping:
struct Meters(f64);
impl std::fmt::Display for Meters {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.2}m", self.0)
}
}
Best Practices
- Prefer static dispatch (generics) for performance-critical paths; use
dyn Traitwhen you need heterogeneous collections or to reduce binary size. - Use associated types for traits where each type has exactly one implementation (like
Iterator::Item). - Keep trait definitions small and focused — prefer composing multiple small traits over one large one.
- Derive common traits (
Debug,Clone,PartialEq,Eq,Hash) whenever possible. - Use
whereclauses for readability when bounds get complex. - Implement
From/Intofor type conversions rather than ad-hoc methods.
Common Pitfalls
- Trait object safety: Traits with generic methods or methods returning
Selfare not object-safe and cannot be used asdyn Trait. Workarounds include using associated types orBox<dyn Trait>return types. - Orphan rule: You cannot implement a foreign trait on a foreign type. Use the newtype pattern as a workaround.
- Monomorphization bloat: Heavy use of generics can increase binary size since each instantiation generates separate code. Consider
dyn Traitor extracting non-generic inner functions. - Overly broad bounds: Adding too many trait bounds to generics makes APIs rigid. Only require what the function actually uses.
- Forgetting
+ Send + Sync: Trait objects used across threads need explicitdyn Trait + Send + Syncbounds.
Install this skill directly: skilldb add rust-skills
Related Skills
Async Rust
Async Rust programming with async/await, Tokio runtime, futures, and concurrent task patterns
Cargo Workspace
Cargo workspaces, project structure, dependency management, and multi-crate Rust project organization
Error Handling
Rust error handling with Result, Option, the ? operator, and ecosystem crates anyhow and thiserror
Lifetimes
Rust lifetime annotations for ensuring reference validity and understanding the borrow checker
Ownership Borrowing
Rust ownership, borrowing, and move semantics for writing memory-safe code without a garbage collector
Pattern Matching
Rust pattern matching with match, if let, while let, destructuring, and advanced match patterns