Traits and Generics
Traits define shared behaviour. Generics let you write code that works for many types. Together they give you abstraction with zero runtime cost.
For Python developers: traits are close to 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 is 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 behaviour (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
Continue to 10-modules-crates.md to learn how to organise code into modules, 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 { }