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 Rust's concurrency model. Next, we'll explore how to write safe concurrent code using threads, channels, and shared state.
Ready to continue? Head to Concurrency!