Traits & Generics
Traits are Rust's version of interfaces — they describe what a type CAN DO. Generics let you write code that works with any type that satisfies a trait.
What Is a Trait?
MyTrait, you promise it has method_a() and method_b(). This lets you write functions that work with ANY type that fulfills the contract.
1 │ trait Speak {
2 │ fn speak(&self) -> String;
3 │ }
4 │
5 │ struct Dog { name: String }
6 │ struct Cat { name: String }
7 │
8 │ impl Speak for Dog {
9 │ fn speak(&self) -> String {
10 │ format!("{} says: Woof!", self.name)
11 │ }
12 │ }
13 │
14 │ impl Speak for Cat {
15 │ fn speak(&self) -> String {
16 │ format!("{} says: Meow!", self.name)
17 │ }
18 │ }
19 │
20 │ let dog = Dog { name: "Rex".to_string() };
21 │ let cat = Cat { name: "Whiskers".to_string() };
22 │ println!("{}", dog.speak());
23 │ println!("{}", cat.speak());
trait defines a shared interface
5-6 two different structs
8-12 impl Speak for Dog gives Dog its own version
14-18 same trait, different behavior for Cat
22-23 same method name, different results
Default Methods
trait Greet {
fn greet(&self) -> String {
"Hello!".to_string() // default
}
}
struct Person { name: String }
struct Robot;
impl Greet for Person {
fn greet(&self) -> String {
format!("Hi, I'm {}!", self.name)
}
}
// Robot uses the default
impl Greet for Robot {}
println!("{}", Person { name: "Alice".to_string() }.greet());
println!("{}", Robot.greet()); // "Hello!"
Generics (Trait Bounds)
T: Speak means "T can be any type, as long as it has the Speak trait." The compiler creates a specialized version for each type you use — no runtime cost.
1 │ // T must implement Speak
2 │ fn make_it_speak(thing: &impl Speak) {
3 │ println!("{}", thing.speak());
4 │ }
5 │
6 │ // Same thing, different syntax (trait bound)
7 │ fn make_it_speak_v2(thing: &T) {
8 │ println!("{}", thing.speak());
9 │ }
10 │
11 │ // Multiple bounds
12 │ fn show_and_speak(thing: &(impl Speak + std::fmt::Display)) { }
13 │
14 │ // Where clause (clean with many bounds)
15 │ fn complex(a: &T, b: &U)
16 │ where
17 │ T: Speak + std::fmt::Debug,
18 │ U: Speak + Clone,
19 │ { }
20 │
21 │ // Return impl Trait
22 │ fn get_speaker() -> impl Speak {
23 │ Dog { name: "Rex".to_string() }
24 │ }
&impl Speak = any type with that trait
7 T: Speak = named trait bound syntax
12 + requires multiple traits
15-19 where clause keeps signatures clean
22 impl Speak as return type
Generic Structs
struct Pair {
first: T,
second: T,
}
impl Pair {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
// Only for Pair that implements Display + PartialOrd
impl Pair {
fn print_larger(&self) {
if self.first >= self.second {
println!("{}", self.first);
} else {
println!("{}", self.second);
}
}
}
Derive Macros
#[derive()]. You write the annotation, Rust writes the code. Do this instead of writing boilerplate.
1 │ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2 │ struct Point {
3 │ x: i32,
4 │ y: i32,
5 │ }
6 │
7 │ // Debug — lets you print with {:?}
8 │ // Clone — lets you call .clone()
9 │ // Copy — copies on assignment instead of moving
10 │ // PartialEq — lets you use == and !=
11 │ // Eq — full equality (no NaN-like values)
12 │ // Hash — works with HashMap/HashSet keys
13 │ // Default — lets you call Point::default()
14 │ // Ord — lets you sort
15 │
16 │ // You can also derive on enums
17 │ #[derive(Debug, Clone, PartialEq)]
18 │ enum Color { Red, Green, Blue }
#[derive(...)] auto-implements multiple traits
3-5 simple struct — one line replaces tons of boilerplate
7-14 each derive adds a capability
17 works on enums too
Common Standard Traits
| Trait | What It Does | Example |
Display | Format for users ({}) | println!("{x}") |
Debug | Format for developers ({:?}) | println!("{x:?}") |
Clone | Make a copy with .clone() | x.clone() |
Copy | Copy on assignment (no move) | let y = x; |
PartialEq | Compare with == | x == y |
Eq | Full equality (used with PartialEq) | derive both |
Ord / PartialOrd | Sorting / comparing order | x > y, .sort() |
Hash | Use as HashMap key | derive |
From / Into | Convert between types | String::from("hi") |
Default | Create a default value | Type::default() |
Display vs Debug
use std::fmt;
struct User { name: String, age: u8 }
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({})", self.name, self.age)
}
}
println!("{}", user); // "Alice (30)"
From / Into
struct Username(String);
impl From for Username {
fn from(s: String) -> Self {
Username(s.to_lowercase())
}
}
let name = Username::from("Alice".to_string());
// or
let name: Username = "Alice".to_string().into();
Trait Objects (dyn Trait)
Box<dyn Trait>. This stores data on the heap and allows dynamic dispatch (the method is looked up at runtime). There's a tiny performance cost.
1 │ trait Animal {
2 │ fn make_sound(&self) -> String;
3 │ }
4 │
5 │ struct Dog;
6 │ struct Cat;
7 │
8 │ impl Animal for Dog {
9 │ fn make_sound(&self) -> String { "Woof".to_string() }
10 │ }
11 │ impl Animal for Cat {
12 │ fn make_sound(&self) -> String { "Meow".to_string() }
13 │ }
14 │
15 │ // Vec of different types that all implement Animal
16 │ let animals: Vec<Box<dyn Animal>> = vec![
17 │ Box::new(Dog),
18 │ Box::new(Cat),
19 │ ];
20 │
21 │ for animal in &animals {
22 │ println!("{}", animal.make_sound());
23 │ }
Box<dyn Animal> = heap-allocated trait object
17-18 different types in one Vec
21-22 dynamic dispatch at runtime
Try It Yourself
Task 1: Define a Trait
Create a trait called Area with a method area(&self) -> f64. Implement it for Circle (take radius) and Rectangle (take width and height).
trait Area {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
// your implementations here
Show solution
impl Area for Circle {
fn area(&self) -> f64 {
3.14159 * self.radius * self.radius
}
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
let shapes: Vec> = vec![
Box::new(Circle { radius: 2.0 }),
Box::new(Rectangle { width: 3.0, height: 4.0 }),
];
for shape in &shapes {
println!("Area: {}", shape.area());
}
}
Task 2: Generic Function
Write a generic function largest that takes a slice of items and returns the largest. It should work with any type that can be compared.
fn largest(list: &[T]) -> &T {
// your code here
// hint: T must implement PartialOrd
}
Show solution
fn largest(list: &[T]) -> &T {
let mut max = &list[0];
for item in list.iter() {
if item > max {
max = item;
}
}
max
}
Task 3: Convert Temperatures
Create a Temperature enum with Celsius(f64) and Fahrenheit(f64). Implement From for converting between them.
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
// implement From for Fahrenheit
// implement From for Celsius
Show solution
impl From for f64 {
fn from(t: Temperature) -> f64 {
match t {
Temperature::Celsius(c) => c * 9.0 / 5.0 + 32.0,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
// Usage
let c = Temperature::Celsius(100.0);
let f: f64 = c.into(); // 212.0
Task 4: Printable List
Create a generic PrintableList wrapper around Vec<T> and implement Display for it. Items should be comma-separated.
struct PrintableList(Vec);
impl std::fmt::Display for PrintableList {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// your code: write items separated by ", "
}
}
Show solution
impl std::fmt::Display for PrintableList {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut first = true;
for item in &self.0 {
if !first { write!(f, ", ")?; }
write!(f, "{}", item)?;
first = false;
}
Ok(())
}
}
let list = PrintableList(vec![1, 2, 3]);
println!("{list}"); // "1, 2, 3"
Summary
- Traits define shared behavior — any type can implement any trait
- Generics let functions work with any type that satisfies a trait bound
#[derive()]auto-implements common traits (Debug, Clone, Copy, PartialEq)dyn Traitfor collections of mixed typeswhereclauses keep complex bounds readableimpl Traitas return type hides the concrete type