PHASE 2 ← Back to Course
7 / 17
🧬

Traits and Generics

Define shared behavior with traits and write flexible code with generics and trait bounds.

1

Defining Traits with Default Implementations

A trait defines shared behavior by declaring method signatures that types can implement. Traits can also provide default implementations that implementing types may override.

Rust
// Basic trait definition
trait Animal {
    fn speak(&self) -> String;
    // Default implementation
    fn introduce(&self) -> String {
        format!("Hello, I'm an animal!")
    }
struct Dog;
impl Animal for Dog {
    fn speak(&self) -> String {
        String::from("Woof!")
    }
struct Cat;
impl Animal for Cat {
    fn speak(&self) -> String {
        String::from("Meow!")
    }
    // Override default implementation
    fn introduce(&self) -> String {
        format!("{} {}", self.speak(), "I'm a cat!")
    }
let dog = Dog;
println!("{}", dog.speak());     // Woof!
println!("{}", dog.introduce()); // Hello, I'm an animal!
let cat = Cat;
println!("{}", cat.speak());     // Meow!
println!("{}", cat.introduce()); // Meow! I'm a cat!

Traits can have multiple methods, and default implementations can call other methods in the same trait:

Rust
trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
    fn describe(&self) -> String {
        format!("Area: {}, Perimeter: {}", self.area(), self.perimeter())
    }
struct Rectangle {
    width: f64,
    height: f64,
impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }
let rect = Rectangle { width: 5.0, height: 3.0 };
println!("{}", rect.describe());
💡

Default Methods Call Other Methods

A default method can call required methods on self. This lets you provide rich behavior from just a few required method implementations.

2

Trait Bounds and Where Clauses

Trait bounds constrain generic types so they must implement specific traits. Where clauses provide a cleaner syntax when bounds become complex.

Rust
use std::fmt::Display;
// Simple trait bound
fn print_item<T: Display>(item: T) {
    println!("Item: {}", item);
print_item(42);
print_item("hello");
// Multiple trait bounds
fn compare_and_print<T: PartialOrd + Display>(a: T, b: T) {
    if a > b {
        println!("{} is greater", a);
    } else {
        println!("{} is greater", b);
    }
}

When trait bounds grow complex, the where clause moves them after the function signature for clarity:

Rust
trait Summary {
    fn summarize(&self) -> String;
struct Article {
    title: String,
    content: String,
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}", self.title)
    }
// Using where clause for clarity
fn notify<T, U>(item: &T, other: &U) -> String
where
    T: Summary + Display,
    U: Summary,
{
    format!("{} and {}", item.summarize(), other.summarize())
// Conditional trait implementations with where clause
struct Pair<T> {
    x: T,
    y: T,
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
let pair = Pair { x: 5, y: 3 };
pair.cmp_display();
// Multiple where conditions
fn process<T, U, V>(a: T, b: U, c: V)
where
    T: Display + Clone,
    U: Display + Default,
    V: Display + Send + Sync,
{
    println!("{}, {}, {}", a, b, c);
}
💡

When to Use Where Clauses

Use where clauses whenever you have more than one or two trait bounds, or when bounds apply to multiple generic parameters. They keep function signatures readable.

3

Generic Functions and Structs

Generics let you write code that works with many types. The compiler generates specialized versions for each concrete type used (monomorphization), so there is no runtime cost.

Rust
// Generic function
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }
    largest
println!("{}", largest(&[34, 50, 25, 100, 65])); // 100
println!("{}", largest(&[102.4, 34.2, 5000.2])); // 5000.2
Rust
// Generic struct
struct Point<T> {
    x: T,
    y: T,
let point_int = Point { x: 5, y: 10 };
let point_float = Point { x: 1.0, y: 4.0 };
// Multiple type parameters
struct MultiPoint<T, U> {
    x: T,
    y: U,
let point = MultiPoint { x: 5, y: 4.0 };
// Generic methods
impl<T: std::fmt::Display> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
    fn print_coords(&self) {
        println!("x: {}, y: {}", self.x, self.y);
    }
// Specialized implementation for f64 only
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
let p = Point { x: 3.0, y: 4.0 };
println!("Distance: {}", p.distance_from_origin()); // 5.0
Rust
// Generic enum
enum Option<T> {
    Some(T),
    None,
enum Result<T, E> {
    Ok(T),
    Err(E),
let num: Option<i32> = Some(5);
let text: Option<&str> = None;

Zero-Cost Generics

Rust's generics use monomorphization at compile time. Point<i32> and Point<f64> become separate types with no runtime overhead, just like hand-written specialized code.

4

impl Trait Syntax

The impl Trait syntax provides a concise way to specify that a parameter or return type implements a trait, without naming the concrete type.

Rust
use std::fmt::Display;
// impl Trait return type - concrete type at compile time
fn returns_summarizable() -> impl Display {
    42
// Returning different types that implement trait
fn get_value(is_int: bool) -> Box<dyn Display> {
    if is_int {
        Box::new(42)
    } else {
        Box::new("hello")
    }
// impl Trait in parameters (trait bound shorthand)
fn print_it(val: impl Display) {
    println!("{}", val);
// More flexible than specifying generic type
fn combine(a: impl Display, b: impl Display) -> String {
    format!("{}{}", a, b)
println!("{}", combine(5, "hello")); // 5hello
println!("{}", combine("a", "b"));     // ab
Rust
// impl Trait with multiple bounds
fn process(item: impl Display + Clone) -> impl Display {
    item.clone()
// Using in iterators
fn skip_falsy<'a>(v: &'a Vec<i32>) -> impl Iterator<Item = &'a i32> {
    v.iter().filter(|&&x| x != 0)
let nums = vec![1, 0, 2, 0, 3];
for n in skip_falsy(&nums) {
    println!("{}", n);
}
⚠️

impl Trait vs dyn Trait

impl Trait resolves to a single concrete type at compile time (static dispatch). dyn Trait uses a vtable for dynamic dispatch at runtime. Use impl Trait when the type is fixed; use dyn Trait when you need heterogeneous collections.

5

Trait Objects (dyn Trait) vs Static Dispatch

Static dispatch (generics) monomorphizes code for each concrete type, yielding maximum speed but larger binaries. Dynamic dispatch (trait objects) uses a pointer and vtable, enabling heterogeneous collections at a small runtime cost.

Rust
trait Animal {
    fn speak(&self) -> String;
struct Dog;
impl Animal for Dog {
    fn speak(&self) -> String { String::from("Woof") }
struct Cat;
impl Animal for Cat {
    fn speak(&self) -> String { String::from("Meow") }
// Static dispatch: monomorphization at compile time
// Compiler generates separate code for each type
fn static_animal<T: Animal>(animal: T) {
    println!("{}", animal.speak());
static_animal(Dog); // Generates Dog version
static_animal(Cat); // Generates Cat version
// Dynamic dispatch: trait objects
// Single runtime lookup, smaller binary
fn dynamic_animal(animal: &dyn Animal) {
    println!("{}", animal.speak());
let dog = Dog;
let cat = Cat;
dynamic_animal(&dog);
dynamic_animal(&cat);
Rust
// Trait object collections
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
for animal in animals {
    println!("{}", animal.speak());
// Trait objects with ownership
let mut animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];
for animal in &animals {
    println!("{}", animal.speak());
}
💡

Object Safety Rules

A trait can be used as a trait object (dyn Trait) only if it is object-safe. This requires: the return type is not Self, there are no generic type parameters, and static methods have a where Self: Sized bound.

Rust
// Safe trait - can be used as dyn Drawable
trait Drawable {
    fn draw(&self);
// NOT object-safe - has Self return type
trait Creator {
    fn create(&self) -> Self;
    // Cannot use: dyn Creator
// Making it safe with where clause
trait SafeCreator {
    fn create(&self) -> String
    where
        Self: Sized,
    ;
// Can use: dyn SafeCreator
6

Common Standard Library Traits

Rust's standard library defines many useful traits. Knowing these is essential for writing idiomatic Rust code.

Rust
// Display and Debug
#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
use std::fmt;
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} ({})", self.name, self.age)
    }
let p = Person { name: String::from("Alice"), age: 30 };
println!("{}", p);   // Display: Alice (30)
println!("{:?}", p); // Debug: Person { name: "Alice", age: 30 }
Rust
// Clone and Copy
#[derive(Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
let p1 = Point { x: 5, y: 10 };
let p2 = p1;          // Copy: p1 still valid
let p3 = p1.clone(); // Explicit clone
// From and Into traits
struct Number(i32);
impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number(item)
    }
let num: Number = 5.into(); // Automatically uses From impl for Into
// Default trait
#[derive(Default)]
struct Config {
    name: String,
    value: i32,
let config = Config::default();
Rust
// Iterator trait
struct Counter {
    count: u32,
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
let mut counter = Counter { count: 0 };
while let Some(val) = counter.next() {
    println!("{}", val); // 1 2 3 4 5
// PartialEq, Eq, and Ordering traits
#[derive(PartialEq, Eq)]
struct Value(i32);
let a = Value(5);
let b = Value(5);
assert_eq!(a, b);
use std::cmp::Ordering;
#[derive(PartialOrd, Ord, PartialEq, Eq)]
struct Item(i32);
let a = Item(5);
let b = Item(10);
assert_eq!(a.cmp(&b), Ordering::Less);

Derive Macros Save Time

Use #[derive(...)] to automatically implement common traits like Debug, Clone, Copy, PartialEq, Eq, Default, and ordering traits. Only write manual implementations when you need custom behavior.

7

Supertraits

A supertrait is a trait that another trait depends on. If trait B requires trait A, then any type implementing B must also implement A.

Rust
use std::fmt::Display;
// Supertrait: OutlinePrint requires Display
trait OutlinePrint: Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
struct Point {
    x: i32,
    y: i32,
impl Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
impl OutlinePrint for Point {}
let p = Point { x: 5, y: 10 };
p.outline_print();
Rust
// Multiple supertrait bounds
trait Drawable: Display {
    fn draw(&self);
trait Printable: Display {
    fn print(&self);
trait Document: Drawable + Printable {
    fn save(&self);
// Using supertrait in bounds
fn process<T: OutlinePrint>(item: T) {
    // T automatically has Display methods available
    let s = item.to_string();
    item.outline_print();
}
💡

Supertrait Hierarchy

Supertraits create a hierarchy: if Document: Drawable + Printable and both require Display, then any type implementing Document must implement all three. Think of it as trait inheritance.

8

Polymorphism Patterns

Rust supports polymorphism through generics and trait objects. Here are the most common patterns used in real-world Rust code.

Rust
trait Vehicle {
    fn start(&self);
    fn stop(&self);
struct Car;
impl Vehicle for Car {
    fn start(&self) { println!("Car engine starts"); }
    fn stop(&self) { println!("Car engine stops"); }
struct Bike;
impl Vehicle for Bike {
    fn start(&self) { println!("Bike engine starts"); }
    fn stop(&self) { println!("Bike engine stops"); }
// Pattern 1: Generic polymorphism (static dispatch)
fn operate<V: Vehicle>(vehicle: V) {
    vehicle.start();
    vehicle.stop();
operate(Car);
operate(Bike);
Rust
// Pattern 2: Trait object polymorphism (dynamic dispatch)
fn fleet_start(vehicles: &[Box<dyn Vehicle>]) {
    for vehicle in vehicles {
        vehicle.start();
    }
let vehicles: Vec<Box<dyn Vehicle>> = vec![
    Box::new(Car),
    Box::new(Bike),
];
fleet_start(&vehicles);
// Pattern 3: Factory pattern with trait objects
struct Factory;
impl Factory {
    fn create_vehicle(vehicle_type: &str) -> Box<dyn Vehicle> {
        match vehicle_type {
            "car" => Box::new(Car),
            "bike" => Box::new(Bike),
            _ => panic!("Unknown vehicle"),
        }
    }
let vehicle = Factory::create_vehicle("car");
vehicle.start();
Rust
// Pattern 4: Decorator pattern
trait Logger {
    fn log(&self, msg: &str);
struct ConsoleLogger;
impl Logger for ConsoleLogger {
    fn log(&self, msg: &str) {
        println!("LOG: {}", msg);
    }
struct FileLogger;
impl Logger for FileLogger {
    fn log(&self, msg: &str) {
        println!("FILE: {}", msg);
    }
fn do_work(logger: &dyn Logger) {
    logger.log("Starting work");
    logger.log("Work complete");
do_work(&ConsoleLogger);
do_work(&FileLogger);

Choosing the Right Pattern

Use generics (static dispatch) when you know types at compile time and want maximum performance. Use trait objects (dynamic dispatch) when you need heterogeneous collections or runtime flexibility, like the factory and decorator patterns above.

← Previous Chapter 7 of 17 Next →