Traits and Generics in Rust
Master Rust traits and generics — defining traits, trait bounds, default methods, trait objects, and generic data structures.
What you'll learn
- ✓How to define and implement traits
- ✓Trait bounds and where clauses for generics
- ✓Static dispatch vs dynamic dispatch (trait objects)
- ✓Common standard library traits
Prerequisites
- •Rust basics (structs, enums, functions)
- •Understanding of ownership and references
Traits are Rust’s mechanism for shared behavior — similar to interfaces in Go or Java, but with default implementations, associated types, and zero-cost static dispatch.
Defining a trait
trait Summary {
fn summarize(&self) -> String;
}
Implementing a trait
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}...", self.title, &self.content[..50])
}
}
struct Tweet {
username: String,
text: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.text)
}
}
Default methods
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// summarize() uses the default implementation
}
Traits as parameters
impl Trait syntax
fn notify(item: &impl Summary) {
println!("Breaking: {}", item.summarize());
}
Trait bound syntax
fn notify<T: Summary>(item: &T) {
println!("Breaking: {}", item.summarize());
}
Multiple bounds
fn notify(item: &(impl Summary + Display)) {
println!("{}", item);
}
// or with trait bound syntax
fn notify<T: Summary + Display>(item: &T) {
println!("{}", item);
}
Where clauses
For complex bounds, use where for readability.
fn process<T, U>(t: &T, u: &U) -> String
where
T: Summary + Clone,
U: Display + Debug,
{
format!("{} — {:?}", t.summarize(), u)
}
Returning impl Trait
fn create_summarizable() -> impl Summary {
Tweet {
username: "bot".to_string(),
text: "Hello world".to_string(),
}
}
The caller does not know the concrete type — only that it implements Summary.
Generics
Generic functions
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in &list[1..] {
if item > max {
max = item;
}
}
max
}
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest: {}", largest(&chars));
Generic structs
struct Point<T> {
x: T,
y: T,
}
impl<T: Display> Point<T> {
fn print(&self) {
println!("({}, {})", self.x, self.y);
}
}
impl Point<f64> {
fn distance(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Multiple generic types
struct Pair<A, B> {
first: A,
second: B,
}
Trait objects (dynamic dispatch)
When you need to store different types that implement the same trait, use dyn Trait.
fn get_items() -> Vec<Box<dyn Summary>> {
vec![
Box::new(Article {
title: "AI News".to_string(),
content: "Artificial intelligence is transforming...".to_string(),
}),
Box::new(Tweet {
username: "rustlang".to_string(),
text: "Rust 2024 edition is here!".to_string(),
}),
]
}
Static vs dynamic dispatch
| Feature | impl Trait / generics | dyn Trait |
|---|---|---|
| Dispatch | Static (monomorphized) | Dynamic (vtable) |
| Performance | Zero-cost | Pointer indirection |
| Concrete type known at | Compile time | Runtime |
| Can store mixed types | No | Yes |
Use generics (static dispatch) by default. Use dyn when you need a collection of mixed types or the concrete type is decided at runtime.
Common standard library traits
| Trait | Purpose |
|---|---|
Display | Format for users ({}) |
Debug | Format for developers ({:?}) |
Clone | Deep copy with .clone() |
Copy | Implicit bit-copy on assignment |
PartialEq, Eq | Equality comparison |
PartialOrd, Ord | Ordering comparison |
Hash | Hash for use in HashMap/HashSet |
Default | Provide a default value |
From/Into | Type conversions |
Iterator | Iteration protocol |
Deriving traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
name: String,
age: u32,
}
Associated types
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 <= 5 {
Some(self.count)
} else {
None
}
}
}
Summary
Traits define shared behavior. Generics with trait bounds give you static dispatch and zero-cost abstractions. Trait objects (dyn Trait) give you dynamic dispatch when you need runtime polymorphism. Start with generics, reach for trait objects when the concrete type cannot be known at compile time, and derive standard traits to get common functionality for free.
Related articles
- Rust Rust Traits: Shared Behavior Without Inheritance
Understand how Rust traits define shared behavior, with default methods, trait bounds, derive, and a clear take on dyn Trait versus impl Trait.
- Rust Rust Pattern Matching: Advanced Techniques and Guards
Go beyond basic match with pattern guards, bindings, nested patterns, or-patterns, and real-world refactoring strategies in Rust.
- Rust Trait Objects vs Generics in Rust: A Decision Guide
Understand the trade-offs between dynamic dispatch with trait objects and static dispatch with generics in Rust through real-world examples.
- Rust Error Handling with Result and Option in Rust
Handle errors idiomatically in Rust — Result, Option, the ? operator, custom error types, and the thiserror/anyhow ecosystem.