Traits & Generics in Rust
Traits and generics are the foundation of abstraction in Rust. Traits define shared behavior -- similar to interfaces in other languages -- while generics let you write code that works with many types. Together, they enable you to write flexible, reusable, and type-safe code without sacrificing performance.
Defining a Trait
A trait declares a set of methods that a type must implement. Think of it as a contract: any type that implements the trait promises to provide those methods:
trait Summary {
fn summarize(&self) -> String;
// Default implementation -- types can override this
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..20.min(self.summarize().len())])
}
}
struct Article {
title: String,
author: String,
content: String,
}
struct Tweet {
username: String,
body: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {} -- {}", self.title, self.author, &self.content[..50.min(self.content.len())])
}
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.body)
}
}
fn main() {
let article = Article {
title: String::from("Rust 2024 Edition Released"),
author: String::from("The Rust Team"),
content: String::from("The Rust programming language has released its 2024 edition with many improvements."),
};
let tweet = Tweet {
username: String::from("rustlang"),
body: String::from("Exciting news for the Rust community!"),
};
println!("Article: {}", article.summarize());
println!("Tweet: {}", tweet.summarize());
}
Implementing Standard Library Traits
Rust's standard library defines many useful traits. Implementing them lets your types integrate naturally with the language:
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
struct Color {
r: u8,
g: u8,
b: u8,
}
// Implement Display for human-readable output
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
}
}
fn main() {
let red = Color { r: 255, g: 0, b: 0 };
let also_red = red.clone();
let blue = Color { r: 0, g: 0, b: 255 };
// Display trait gives us to_string() and works with println!
println!("Red: {}", red);
println!("Blue: {}", blue);
// Debug trait gives us {:?} formatting
println!("Debug: {:?}", red);
// PartialEq gives us == and !=
println!("red == also_red: {}", red == also_red);
println!("red == blue: {}", red == blue);
}
Generic Functions
Generics let you write a function once that works with many types. The compiler generates specialized code for each type used, so there is no runtime cost:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in &list[1..] {
if item > largest {
largest = item;
}
}
largest
}
fn first_and_last<T: std::fmt::Debug>(items: &[T]) -> Option<(&T, &T)> {
if items.is_empty() {
None
} else {
Some((&items[0], &items[items.len() - 1]))
}
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", largest(&chars));
let words = vec!["apple", "zebra", "mango"];
println!("Largest word: {}", largest(&words));
if let Some((first, last)) = first_and_last(&numbers) {
println!("First: {:?}, Last: {:?}", first, last);
}
}
Trait Bounds
Trait bounds constrain generic types so you can only use types that provide the behavior you need. There are two equivalent syntaxes:
use std::fmt;
// Syntax 1: Inline trait bound
fn print_labeled<T: fmt::Display>(label: &str, value: T) {
println!("{}: {}", label, value);
}
// Syntax 2: where clause (cleaner for complex bounds)
fn debug_pair<T, U>(first: T, second: U)
where
T: fmt::Debug + Clone,
U: fmt::Debug + fmt::Display,
{
println!("Debug: {:?} and {:?}", first, second);
println!("Display second: {}", second);
let _cloned = first.clone();
}
// Multiple trait bounds with +
fn compare_and_display<T: PartialOrd + fmt::Display>(a: T, b: T) {
if a > b {
println!("{} is greater than {}", a, b);
} else if a < b {
println!("{} is less than {}", a, b);
} else {
println!("{} equals {}", a, b);
}
}
fn main() {
print_labeled("Name", "Rust");
print_labeled("Version", 2024);
debug_pair(vec![1, 2, 3], "hello");
compare_and_display(10, 20);
compare_and_display(3.14, 2.71);
compare_and_display("apple", "banana");
}
impl Trait Syntax
The impl Trait syntax provides a convenient shorthand. In function parameters, it is syntactic sugar for a generic with a trait bound. In return position, it lets you return a concrete type without naming it:
// In parameter position: shorthand for generics
fn notify(item: &impl std::fmt::Display) {
println!("Breaking news: {}", item);
}
// In return position: return some type that implements the trait
fn make_greeting(name: &str) -> impl std::fmt::Display {
format!("Hello, {}! Welcome to Rust.", name)
}
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn main() {
notify(&"Rust is awesome");
notify(&42);
let greeting = make_greeting("Alice");
println!("{}", greeting);
let add_five = make_adder(5);
println!("5 + 3 = {}", add_five(3));
println!("5 + 10 = {}", add_five(10));
}
Generic Structs and Implementations
Structs can be generic over one or more type parameters. You can then write implementations that apply to all types or only to specific ones:
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
fn into_tuple(self) -> (T, T) {
(self.first, self.second)
}
}
// This impl block only applies when T implements Display + PartialOrd
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.first >= self.second {
&self.first
} else {
&self.second
}
}
fn display(&self) {
println!("({}, {})", self.first, self.second);
}
}
fn main() {
let int_pair = Pair::new(10, 20);
int_pair.display();
println!("Larger: {}", int_pair.larger());
let str_pair = Pair::new("hello", "world");
str_pair.display();
println!("Larger: {}", str_pair.larger());
let (a, b) = Pair::new(3.14, 2.71).into_tuple();
println!("Unpacked: {} and {}", a, b);
}
Trait Objects for Dynamic Dispatch
When you need a collection of different types that share a trait, use trait objects with dyn. This uses dynamic dispatch (a vtable lookup at runtime) instead of static dispatch:
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing circle with radius {:.1}", self.radius);
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Drawable for Rectangle {
fn draw(&self) {
println!("Drawing rectangle {}x{}", self.width, self.height);
}
fn area(&self) -> f64 {
self.width * self.height
}
}
fn print_total_area(shapes: &[Box<dyn Drawable>]) {
let total: f64 = shapes.iter().map(|s| s.area()).sum();
println!("Total area: {:.2}", total);
}
fn main() {
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 4.0, height: 6.0 }),
Box::new(Circle { radius: 3.0 }),
];
for shape in &shapes {
shape.draw();
println!(" Area: {:.2}", shape.area());
}
print_total_area(&shapes);
}
Try It Yourself
Create a trait-based system and experiment with adding new types that implement the trait:
use std::fmt;
trait Animal: fmt::Display {
fn name(&self) -> &str;
fn sound(&self) -> &str;
fn legs(&self) -> u32;
fn describe(&self) {
println!(
"{} says '{}' and walks on {} legs",
self.name(),
self.sound(),
self.legs()
);
}
}
struct Dog { name: String }
struct Cat { name: String }
struct Spider { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> &str { "Woof!" }
fn legs(&self) -> u32 { 4 }
}
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> &str { "Meow!" }
fn legs(&self) -> u32 { 4 }
}
impl Animal for Spider {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> &str { "..." }
fn legs(&self) -> u32 { 8 }
}
impl fmt::Display for Dog {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Dog({})", self.name) }
}
impl fmt::Display for Cat {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Cat({})", self.name) }
}
impl fmt::Display for Spider {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Spider({})", self.name) }
}
fn most_legs(animals: &[Box<dyn Animal>]) -> &dyn Animal {
animals.iter()
.max_by_key(|a| a.legs())
.map(|a| a.as_ref())
.unwrap()
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: String::from("Rex") }),
Box::new(Cat { name: String::from("Whiskers") }),
Box::new(Spider { name: String::from("Charlotte") }),
];
for animal in &animals {
animal.describe();
}
let winner = most_legs(&animals);
println!("\nMost legs: {} with {} legs", winner, winner.legs());
}
Key Takeaways
- Traits define shared behavior as a set of methods that types must implement
- Default method implementations reduce boilerplate when many types share common logic
- Generics let you write one function or struct that works with many types, with zero runtime cost
- Trait bounds constrain generics so you can only use types that provide the required behavior
impl Traitprovides convenient shorthand in both parameter and return position- Use
dyn TraitwithBoxfor dynamic dispatch when you need collections of mixed types - Conditional implementations let you add methods only when type parameters satisfy certain bounds
Pro Tip: Prefer static dispatch (generics) when performance matters and you know the types at compile time. Use dynamic dispatch (
dyn Trait) when you need a heterogeneous collection or want to reduce compile times in large codebases.
Next Steps
Traits and generics are the foundation for one of Rust's most elegant abstractions: iterators. Next, we'll explore how the Iterator trait and its adapter methods let you process sequences of data with concise, zero-cost chains.
Next lesson
Iterators
Master Rust's powerful iterator system to write expressive, efficient, and functional-style code
25 min