Rust Traits and Generics: Writing Reusable Code
Traits and generics are the foundation of Rust’s polymorphism and code reuse system. Traits define shared behavior across types (similar to interfaces in other languages), while generics allow functions and structs to operate on multiple types without runtime overhead. Together, they enable zero-cost abstractions — generic code that is as fast as monomorphized, hand-written specializations.
Traits
A trait defines a set of methods that types can implement:
trait Drawable {
fn draw(&self);
// Default implementation
fn label(&self) -> &str {
"untitled"
}
---Implementing Traits
struct Circle {
radius: f64,
---
struct Square {
side: f64,
---
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing a circle with radius {}", self.radius);
}
fn label(&self) -> &str {
"circle"
}
---
impl Drawable for Square {
fn draw(&self) {
println!("Drawing a square with side {}", self.side);
}
---Trait Bounds
Use trait bounds to specify what traits a generic type must implement:
fn render<T: Drawable>(item: &T) {
println!("Rendering: {}", item.label());
item.draw();
---
// Multiple bounds
fn process<T: Drawable + Clone>(item: &T) {
let copy = item.clone();
render(©);
---
// `where` clause for readability
fn complex_function<T, U>(t: &T, u: &U)
where
T: Drawable + Clone,
U: Drawable + std::fmt::Debug,
{
t.draw();
println!("Debug: {:?}", u);
---Generic Functions
Generic functions work with any type that satisfies their bounds:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
---
let numbers = vec![3, 7, 1, 9, 4];
println!("{}", largest(&numbers)); // 9
let strings = vec!["apple", "banana", "cherry"];
println!("{}", largest(&strings)); // "cherry"
Generic Structs and Enums
struct Point<T> {
x: T,
y: T,
---
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
fn x(&self) -> &T {
&self.x
}
---
// Specialized implementation for f64 only
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
---
// Generic enum
enum Result<T, E> {
Ok(T),
Err(E),
---
enum Option<T> {
Some(T),
None,
---Associated Types
Associated types connect a type closely with a trait:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
---
struct Counter {
count: u32,
---
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
---Compare with generic traits:
// Generic trait — requires explicit type annotation
trait Iterator<T> {
fn next(&mut self) -> Option<T>;
---
// Associated type — cleaner when there's a natural relationship
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
---Trait Objects
Trait objects enable dynamic dispatch, allowing heterogeneous collections:
fn render_all(items: &[Box<dyn Drawable>]) {
for item in items {
item.draw();
}
---
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 3.0 }),
];
render_all(&shapes);Static vs Dynamic Dispatch
| Aspect | Generics (Static) | Trait Objects (Dynamic) | |
Associated Types vs Generics
When designing traits, you can use associated types or generic type parameters. Associated types (trait Iterator { type Item; }) enforce that a type implements the trait for exactly one type of item. Generics (trait Container<T>) allow multiple implementations for different T:
// Associated type — each type can only implement Iterator once
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
---
// Generic — a type could implement Container<u32> and Container<String>
trait Container<T> {
fn insert(&mut self, item: T);
---Use associated types when the type is logically part of the trait contract (like Item for iterators). Use generics when the type varies independently of the implementing type (like a key-value store that could store different value types).
Trait Object Safety
A trait is object-safe if it can be used as dyn Trait. Traits are NOT object-safe if they have:
- Methods that return
Selfby value - Generic type parameters
- Associated constants or functions without receivers
trait ObjectSafe {
fn do_something(&self); // OK
fn box_clone(&self) -> Box<dyn ObjectSafe>; // OK (returns Box<Self>)
---
trait NotObjectSafe {
fn new() -> Self; // NOT OK — returns Self
fn generic<T>(&self, val: T); // NOT OK — generic
---Marker Traits and Blanket Implementations
Marker traits have no methods — they tag types with properties. Send and Sync are the most important marker traits. They are automatically implemented when the compiler determines it is safe. You can also use marker traits for zero-cost type-level programming:
trait Validated {}
impl Validated for String {} // Only validated strings get this
fn process(input: &str) -> &str { input }
fn process_validated(input: &(impl Validated + AsRef<str>)) -> &str {
input.as_ref()
---Blanket implementations apply a trait to all types that satisfy a bound. The standard library uses them extensively — ToString is implemented for any type that implements Display.
——–|——————|————————| | Dispatch | Compile-time monomorphization | Runtime vtable lookup | | Performance | Zero-cost | Small overhead per call | | Code Size | Larger (duplicated per type) | Smaller (single function) | | Flexibility | Fixed at compile time | Runtime polymorphism |
Common Trait Patterns
From and Into
impl From<i32> for MyNumber {
fn from(value: i32) -> Self {
MyNumber(value)
}
---
let num = MyNumber::from(42);
let num: MyNumber = 42.into(); // Using Into trait
Deref and DerefMut
Enable smart pointers to behave like references:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
---Display and Debug
use std::fmt;
#[derive(Debug)]
struct Person {
name: String,
age: u32,
---
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({})", self.name, self.age)
}
---Default
#[derive(Default)]
struct Config {
host: String,
port: u16,
debug: bool,
---
// impl Default for Config { ... } is auto-derived
let config = Config::default();Advanced Trait Patterns
Marker Traits
Traits without methods that indicate properties:
trait Serializable {}
trait Send: Send {} // Auto-trait in std
impl Serializable for i32 {}
impl Serializable for String {}
fn save_to_disk<T: Serializable>(value: &T) { /* ... */ }Super Traits
Traits that depend on other traits:
trait Shape: Drawable + Clone {
fn area(&self) -> f64;
---
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
---Blanket Implementations
Implement a trait for all types that satisfy a bound:
impl<T: Display> ToString for T {
// Provided by std
---Best Practices
- Prefer generics with trait bounds over trait objects when the types are known at compile time
- Use associated types when there is a natural one-to-one relationship between a trait and a type
- Keep trait definitions focused — each trait should have a single responsibility
- Use trait bounds on functions rather than on struct definitions when possible
- Prefer derived traits (
#[derive(Debug, Clone, PartialEq)]) over manual implementations - Use
impl Traitin argument position for simple cases, explicit generics for complex bounds
Trait Objects vs Generics
Traits in Rust enable two distinct polymorphism patterns. Generics (static dispatch) monomorphize code at compile time, producing specialized code for each concrete type with zero runtime overhead. Trait objects (dynamic dispatch) use vtables to dispatch methods at runtime, trading a small performance cost for reduced code bloat and the ability to store heterogeneous types in collections. Use generics for performance-critical code and trait objects when you need runtime flexibility or type erasure.
Associated Types vs Generic Parameters
Associated types (trait Iterator { type Item; }) express that a trait has exactly one implementation of the type for a given implementor. Generic parameters allow multiple implementations for the same type. Use associated types when the relationship is one-to-one (like Iterator), and generic parameters when you need multiple implementations or additional configurability.
Coherence and Orphan Rules
Rust’s coherence rules ensure that trait implementations are unambiguous. The orphan rule states that you can implement a trait for a type only if at least one of the trait or the type is local to your crate. This prevents conflicting implementations across crates. The #[derive] macro bypasses this for standard traits, and newtype wrappers can work around orphan rules by wrapping external types.
Conclusion
Traits and generics give Rust powerful, zero-cost abstraction. Use traits to define shared behavior, generics to write type-flexible code, and trait objects for runtime polymorphism. The combination produces reusable, type-safe code without sacrificing performance.
FAQ
When should I use impl Trait vs generic parameters? impl Trait in argument position is syntactic sugar for anonymous generic parameters. Use impl Trait for simple cases and explicit generics when you need to name the type or use it in multiple places.
How do I decide between trait objects and generics? Generics produce faster code via monomorphization but increase binary size. Trait objects use dynamic dispatch with a small performance cost but reduce code bloat. Use generics in hot paths and library APIs; use trait objects for heterogeneous collections.
What is the difference between dyn Trait and impl Trait? dyn Trait is a trait object — a type-erased pointer with dynamic dispatch. impl Trait is a statically-dispatched opaque type. dyn Trait goes behind a pointer (&dyn, Box<dyn>), while impl Trait is a concrete (but hidden) type.
How do I implement multiple traits with the same method name? Use fully qualified syntax: <Type as Trait>::method(self) disambiguates which trait’s method to call.
Related: See our Rust ownership guide and Rust concurrency guide.