TechLead
Lesson 10 of 28
5 min read
Rust

Traits & Generics

Master Rust traits, generics, trait bounds, where clauses, associated types, default implementations, and dynamic dispatch with dyn Trait.

Defining and Implementing Traits

Traits define shared behavior — similar to interfaces in other languages. They specify method signatures that types must implement, enabling polymorphism and code reuse without inheritance.

// Define a trait
trait Summary {
    // Required method — implementors must define this
    fn summarize(&self) -> String;

    // Default implementation — can be overridden
    fn preview(&self) -> String {
        format!("{}...", &self.summarize()[..20.min(self.summarize().len())])
    }
}

struct Article {
    title: String,
    author: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}, by {} — {}", self.title, self.author, &self.content[..50.min(self.content.len())])
    }
    // preview() uses the default implementation
}

struct Tweet {
    username: String,
    content: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }

    fn preview(&self) -> String {
        format!("@{} tweeted", self.username) // Override default
    }
}

fn main() {
    let article = Article {
        title: String::from("Rust 2024"),
        author: String::from("Ferris"),
        content: String::from("Rust continues to grow with amazing new features and improvements"),
    };
    let tweet = Tweet {
        username: String::from("rustlang"),
        content: String::from("Rust 1.80 is out!"),
    };
    println!("{}", article.summarize());
    println!("{}", tweet.summarize());
    println!("{}", tweet.preview());
}

Generics

// Generic function
fn largest(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in &list[1..] {
        if item > largest {
            largest = item;
        }
    }
    largest
}

// Generic struct
struct Point {
    x: T,
    y: T,
}

// Different types for x and y
struct MixedPoint {
    x: T,
    y: U,
}

// Implement methods on generic struct
impl Point {
    fn display(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

// Implement methods only for specific types
impl Point {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("Largest: {}", largest(&numbers));

    let chars = vec!['a', 'z', 'm', 'b'];
    println!("Largest: {}", largest(&chars));

    let int_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.5, y: 3.7 };
    int_point.display();
    float_point.display();
    println!("Distance: {:.2}", float_point.distance_from_origin());

    let mixed = MixedPoint { x: 5, y: 3.14 };
    println!("({}, {})", mixed.x, mixed.y);
}

Trait Bounds and Where Clauses

use std::fmt::{Display, Debug};

// Trait bound syntax
fn print_summary(item: &T) {
    println!("{}", item.summarize());
}

// impl Trait syntax (sugar for simple cases)
fn print_summary_alt(item: &impl Summary) {
    println!("{}", item.summarize());
}

// Multiple bounds with +
fn display_and_debug(item: &T) {
    println!("Display: {item}");
    println!("Debug: {item:?}");
}

// Where clauses for complex bounds
fn complex_function(t: &T, u: &U) -> String
where
    T: Display + Clone,
    U: Debug + Summary,
{
    format!("{t} and {:?}", u)
}

// Returning types that implement traits
fn make_summary() -> impl Summary {
    Tweet {
        username: String::from("bot"),
        content: String::from("Hello from a function!"),
    }
}

trait Summary { fn summarize(&self) -> String; }
struct Tweet { username: String, content: String }
impl Summary for Tweet {
    fn summarize(&self) -> String { format!("@{}: {}", self.username, self.content) }
}

Associated Types and Supertraits

// Associated types — specify a type as part of the trait
trait Iterator {
    type Item; // Associated type
    fn next(&mut self) -> Option;
}

struct Counter {
    count: u32,
    max: u32,
}

impl Iterator for Counter {
    type Item = u32; // Concrete type for this implementation
    fn next(&mut self) -> Option {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

// Supertraits: require another trait
trait PrettyPrint: std::fmt::Display {
    fn pretty_print(&self) {
        println!("=== {} ===", self); // Can use Display methods!
    }
}

// Implement for anything that's Display
impl PrettyPrint for T {}

fn main() {
    let mut counter = Counter { count: 0, max: 5 };
    while let Some(n) = counter.next() {
        print!("{n} ");
    }
    println!();
    42.pretty_print();
    "hello".pretty_print();
}

Trait Objects (Dynamic Dispatch)

trait Drawable {
    fn draw(&self);
    fn area(&self) -> f64;
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Drawable for Circle {
    fn draw(&self) { println!("Drawing circle (r={})", self.radius); }
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}

impl Drawable for Square {
    fn draw(&self) { println!("Drawing square (s={})", self.side); }
    fn area(&self) -> f64 { self.side * self.side }
}

// dyn Trait = dynamic dispatch (runtime polymorphism)
fn draw_all(shapes: &[Box]) {
    for shape in shapes {
        shape.draw();
        println!("  Area: {:.2}", shape.area());
    }
}

// &dyn Trait also works (no heap allocation)
fn print_area(shape: &dyn Drawable) {
    println!("Area: {:.2}", shape.area());
}

fn main() {
    let shapes: Vec> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Square { side: 3.0 }),
        Box::new(Circle { radius: 2.5 }),
    ];
    draw_all(&shapes);

    let circle = Circle { radius: 10.0 };
    print_area(&circle);
}

Key Takeaways

  • ✅ Traits define shared behavior — Rust's alternative to interfaces and inheritance
  • ✅ Generics with trait bounds enable type-safe polymorphism at compile time
  • ✅ Where clauses keep complex trait bounds readable
  • ✅ Associated types simplify traits that have one logical output type
  • ✅ Use dyn Trait for runtime polymorphism when you need heterogeneous collections

Continue Learning