Traits and Generics
Traits define shared behavior across types. Generics let you write code that works with multiple types. Together they give Rust powerful abstraction with zero runtime cost.
For Python developers: Traits are similar to Python's Protocols (PEP 544) or abstract base classes. They define an interface that types must implement.
Traits
Defining a Trait
trait Summary {
fn summarize(&self) -> String;
}
Python comparison:
# Using Protocol (Python 3.8+)
from typing import Protocol
class Summary(Protocol):
def summarize(self) -> str:
...
# Or Abstract Base Class
from abc import ABC, abstractmethod
class Summary(ABC):
@abstractmethod
def summarize(self) -> str:
pass
A trait defines a set of methods that types can implement. Think of it as an interface.
Implementing a Trait
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
title: String,
author: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.content)
}
}
fn main() {
let article = Article {
title: "Rust is Great".to_string(),
author: "Alice".to_string(),
content: "Long article...".to_string(),
};
let tweet = Tweet {
username: "bob".to_string(),
content: "Rust rocks!".to_string(),
};
println!("{}", article.summarize());
println!("{}", tweet.summarize());
}
Default Implementations
trait Summary {
fn summarize_author(&self) -> String;
// Default implementation that calls another trait method
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
struct Article {
title: String,
author: String,
}
impl Summary for Article {
fn summarize_author(&self) -> String {
self.author.clone()
}
// Uses the default summarize()
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// Override the default
fn summarize(&self) -> String {
format!("{}: {}", self.summarize_author(), self.content)
}
}
Multiple Traits
use std::fmt;
trait Printable {
fn print(&self);
}
trait Saveable {
fn save(&self) -> Result<(), String>;
}
struct Document {
content: String,
}
impl Printable for Document {
fn print(&self) {
println!("{}", self.content);
}
}
impl Saveable for Document {
fn save(&self) -> Result<(), String> {
println!("Saving: {}", self.content);
Ok(())
}
}
impl fmt::Display for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Document: {}", self.content)
}
}
Common Standard Library Traits
| Trait | Purpose | Methods |
|---|---|---|
Display | User-facing string output | fmt() |
Debug | Developer-facing output | fmt() (usually #[derive(Debug)]) |
Clone | Explicit deep copy | clone() |
Copy | Implicit copy (stack-only) | (marker trait) |
PartialEq | Equality comparison | eq() |
PartialOrd | Ordering comparison | partial_cmp() |
Default | Default value | default() |
Iterator | Iteration protocol | next() |
From / Into | Type conversion | from() / into() |
Drop | Custom cleanup | drop() |
Implementing Display
use std::fmt;
struct Point {
x: f64,
y: f64,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("{}", p); // (3.0, 4.0)
}
Implementing From/Into
struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Celsius(100.0);
let f: Fahrenheit = boiling.into(); // Uses From automatically
println!("{}°F", f.0); // 212°F
}
Generics
Generic Functions
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in &list[1..] {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
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
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
// Methods only for specific types
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let int_point = Point::new(5, 10);
let float_point = Point::new(1.0, 4.0);
println!("{:?}", int_point);
println!("Distance: {}", float_point.distance_from_origin());
// int_point.distance_from_origin(); // ERROR: only for f64
}
Multiple Generic Types
#[derive(Debug)]
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
fn new(first: T, second: U) -> Self {
Pair { first, second }
}
}
fn main() {
let p = Pair::new("hello", 42);
println!("{:?}", p);
let p2 = Pair::new(3.14, true);
println!("{:?}", p2);
}
Generic Enums
You've already been using these:
// From the standard library
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Trait Bounds
Trait bounds constrain what types a generic can accept.
Basic Trait Bounds
use std::fmt::Display;
// Only types that implement Display
fn print_value<T: Display>(value: T) {
println!("Value: {}", value);
}
// Multiple bounds with +
fn print_and_compare<T: Display + PartialOrd>(a: T, b: T) {
if a > b {
println!("{} is greater", a);
} else {
println!("{} is greater", b);
}
}
fn main() {
print_value(42);
print_value("hello");
print_and_compare(10, 20);
}
Where Clause
When bounds get complex, use where:
use std::fmt::{Display, Debug};
// This is hard to read
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> String {
format!("{}", t)
}
// This is cleaner
fn some_function_clean<T, U>(t: &T, u: &U) -> String
where
T: Display + Clone,
U: Clone + Debug,
{
format!("{}", t)
}
Traits as Parameters
Three ways to accept a trait as a parameter:
trait Summary {
fn summarize(&self) -> String;
}
// 1. impl Trait syntax (simple, common)
fn notify(item: &impl Summary) {
println!("Breaking: {}", item.summarize());
}
// 2. Trait bound syntax (more flexible)
fn notify_bound<T: Summary>(item: &T) {
println!("Breaking: {}", item.summarize());
}
// 3. Enforcing same type for multiple parameters
fn notify_same<T: Summary>(item1: &T, item2: &T) {
// item1 and item2 must be the same type
println!("{} and {}", item1.summarize(), item2.summarize());
}
Returning Traits
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> &str;
}
struct Dog;
impl Animal for Dog {
fn name(&self) -> &str { "Dog" }
fn sound(&self) -> &str { "Woof" }
}
// Return a type that implements Animal
fn create_pet() -> impl Animal {
Dog
}
fn main() {
let pet = create_pet();
println!("{} says {}", pet.name(), pet.sound());
}
Limitation: impl Trait in return position can only return one concrete type.
Trait Objects with dyn
When you need different types at runtime:
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
fn name(&self) -> &str { "Circle" }
}
impl Shape for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
fn name(&self) -> &str { "Rectangle" }
}
// Trait object with dyn
fn print_area(shape: &dyn Shape) {
println!("{}: {:.2}", shape.name(), shape.area());
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 4.0, height: 6.0 }),
];
for shape in &shapes {
print_area(shape.as_ref());
}
}
Static vs Dynamic Dispatch
| Feature | Static (impl Trait / generics) | Dynamic (dyn Trait) |
|---|---|---|
| Dispatch | Compile-time (monomorphization) | Runtime (vtable) |
| Performance | Zero-cost, inlined | Small overhead |
| Binary size | Larger (code per type) | Smaller |
| Mixed types | No (one type per call) | Yes (heterogeneous) |
| Use when | Performance matters | Flexibility matters |
Combining Generics and Traits
Generic Impl with Trait Bounds
#[derive(Debug)]
struct Wrapper<T> {
value: T,
}
impl<T: std::fmt::Display> Wrapper<T> {
fn show(&self) {
println!("Wrapper contains: {}", self.value);
}
}
impl<T: std::fmt::Display + Clone> Wrapper<T> {
fn duplicate(&self) -> Self {
Wrapper {
value: self.value.clone(),
}
}
}
fn main() {
let w = Wrapper { value: 42 };
w.show();
let w2 = w.duplicate();
w2.show();
}
Blanket Implementations
Implement a trait for all types that satisfy some bounds:
trait Greet {
fn greet(&self) -> String;
}
// Implement Greet for anything that implements Display
impl<T: std::fmt::Display> Greet for T {
fn greet(&self) -> String {
format!("Hello, {}!", self)
}
}
fn main() {
println!("{}", 42.greet()); // Hello, 42!
println!("{}", "world".greet()); // Hello, world!
println!("{}", 3.14.greet()); // Hello, 3.14!
}
Lifetimes with Generics
When generics involve references, you may need lifetime annotations:
// The returned reference lives as long as both inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// Struct with lifetime and generic
struct ImportantExcerpt<'a, T> {
part: &'a str,
metadata: T,
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result);
}
let excerpt = ImportantExcerpt {
part: &s1,
metadata: 42,
};
println!("{}: {}", excerpt.part, excerpt.metadata);
}
Practice Exercises
Exercise 1: Describable Trait
Create a Describable trait with a describe() method. Implement it for at least three different structs.
trait Describable {
fn describe(&self) -> String;
}
// Implement for Book, Movie, Song
Exercise 2: Generic Stack
Implement a generic stack data structure:
struct Stack<T> {
elements: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self { /* ... */ }
fn push(&mut self, item: T) { /* ... */ }
fn pop(&mut self) -> Option<T> { /* ... */ }
fn peek(&self) -> Option<&T> { /* ... */ }
fn is_empty(&self) -> bool { /* ... */ }
}
Exercise 3: Sortable Collection
Write a generic function that takes a mutable slice, sorts it, and returns the median value:
fn median<T: PartialOrd + Clone>(data: &mut [T]) -> Option<T> {
// Your code
}
Common Mistakes
1. Missing Trait Bounds
fn print_it<T>(value: T) {
println!("{}", value); // ERROR: T doesn't implement Display
}
Fix: Add T: std::fmt::Display.
2. Returning Different Types with impl Trait
fn make_shape(circle: bool) -> impl Shape {
if circle {
Circle { radius: 5.0 }
} else {
Rectangle { width: 3.0, height: 4.0 } // ERROR: different type
}
}
Fix: Use Box<dyn Shape> instead.
3. Orphan Rule
// Can't implement external trait for external type
impl Display for Vec<i32> { } // ERROR
Fix: Create a wrapper type or your own trait.
Key Takeaways
- Traits define shared behavior (like interfaces)
- Generics let you write type-agnostic code
- Trait bounds constrain what generics can do
- Use
impl Traitfor simple cases,dyn Traitfor heterogeneous collections - Static dispatch (generics) has zero runtime cost
- Dynamic dispatch (
dyn) adds flexibility at a small performance cost - Blanket implementations apply traits broadly
- Derive macros auto-implement common traits
Next Steps
In the next chapter, we'll explore Rust's module system and how to organize code into crates and packages.
Quick Reference
// Define a trait
trait Summary {
fn summarize(&self) -> String;
}
// Implement a trait
impl Summary for MyType {
fn summarize(&self) -> String { ... }
}
// Generic function with trait bound
fn process<T: Display + Clone>(item: T) { }
// Where clause
fn process<T>(item: T) where T: Display + Clone { }
// impl Trait parameter
fn notify(item: &impl Summary) { }
// impl Trait return
fn create() -> impl Summary { }
// Trait object
fn process(item: &dyn Summary) { }
let items: Vec<Box<dyn Summary>> = vec![];
// Lifetime with generic
fn longest<'a, T>(x: &'a str, extra: T) -> &'a str { }