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?

A trait says "any type that implements this trait must have these methods." It's like a contract: if your type implements 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());
Legend: 1-3 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

You can provide a default implementation in the trait definition. Types can override it or just use the default.
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)

A generic function works with any type that meets certain requirements. 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 │ }
Legend: 2 &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

Many traits are so common that Rust can implement them automatically with #[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 }
Legend: 1 #[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

TraitWhat It DoesExample
DisplayFormat for users ({})println!("{x}")
DebugFormat for developers ({:?})println!("{x:?}")
CloneMake a copy with .clone()x.clone()
CopyCopy on assignment (no move)let y = x;
PartialEqCompare with ==x == y
EqFull equality (used with PartialEq)derive both
Ord / PartialOrdSorting / comparing orderx > y, .sort()
HashUse as HashMap keyderive
From / IntoConvert between typesString::from("hi")
DefaultCreate a default valueType::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)

When you need a collection of different types that all implement the same trait, use 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 │ }
Legend: 1-3 define the trait   5-13 two different types, same trait   16 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