Skip to editor content
learningrust.orglesson 14 of 26

Traits & Generics in Rust

Traits and generics are the foundation of abstraction in Rust. Traits define shared behavior -- similar to interfaces in other languages -- while generics let you write code that works with many types. Together, they enable you to write flexible, reusable, and type-safe code without sacrificing performance.

Defining a Trait

A trait declares a set of methods that a type must implement. Think of it as a contract: any type that implements the trait promises to provide those methods:

trait Summary {
    fn summarize(&self) -> String;

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

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

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

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

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

fn main() {
    let article = Article {
        title: String::from("Rust 2024 Edition Released"),
        author: String::from("The Rust Team"),
        content: String::from("The Rust programming language has released its 2024 edition with many improvements."),
    };

    let tweet = Tweet {
        username: String::from("rustlang"),
        body: String::from("Exciting news for the Rust community!"),
    };

    println!("Article: {}", article.summarize());
    println!("Tweet: {}", tweet.summarize());
}

Implementing Standard Library Traits

Rust's standard library defines many useful traits. Implementing them lets your types integrate naturally with the language:

use std::fmt;

#[derive(Debug, Clone, PartialEq)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

// Implement Display for human-readable output
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
    }
}

fn main() {
    let red = Color { r: 255, g: 0, b: 0 };
    let also_red = red.clone();
    let blue = Color { r: 0, g: 0, b: 255 };

    // Display trait gives us to_string() and works with println!
    println!("Red: {}", red);
    println!("Blue: {}", blue);

    // Debug trait gives us {:?} formatting
    println!("Debug: {:?}", red);

    // PartialEq gives us == and !=
    println!("red == also_red: {}", red == also_red);
    println!("red == blue: {}", red == blue);
}

Generic Functions

Generics let you write a function once that works with many types. The compiler generates specialized code for each type used, so there is no runtime cost:

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

fn first_and_last<T: std::fmt::Debug>(items: &[T]) -> Option<(&T, &T)> {
    if items.is_empty() {
        None
    } else {
        Some((&items[0], &items[items.len() - 1]))
    }
}

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

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

    let words = vec!["apple", "zebra", "mango"];
    println!("Largest word: {}", largest(&words));

    if let Some((first, last)) = first_and_last(&numbers) {
        println!("First: {:?}, Last: {:?}", first, last);
    }
}

Trait Bounds

Trait bounds constrain generic types so you can only use types that provide the behavior you need. There are two equivalent syntaxes:

use std::fmt;

// Syntax 1: Inline trait bound
fn print_labeled<T: fmt::Display>(label: &str, value: T) {
    println!("{}: {}", label, value);
}

// Syntax 2: where clause (cleaner for complex bounds)
fn debug_pair<T, U>(first: T, second: U)
where
    T: fmt::Debug + Clone,
    U: fmt::Debug + fmt::Display,
{
    println!("Debug: {:?} and {:?}", first, second);
    println!("Display second: {}", second);
    let _cloned = first.clone();
}

// Multiple trait bounds with +
fn compare_and_display<T: PartialOrd + fmt::Display>(a: T, b: T) {
    if a > b {
        println!("{} is greater than {}", a, b);
    } else if a < b {
        println!("{} is less than {}", a, b);
    } else {
        println!("{} equals {}", a, b);
    }
}

fn main() {
    print_labeled("Name", "Rust");
    print_labeled("Version", 2024);

    debug_pair(vec![1, 2, 3], "hello");

    compare_and_display(10, 20);
    compare_and_display(3.14, 2.71);
    compare_and_display("apple", "banana");
}

impl Trait Syntax

The impl Trait syntax provides a convenient shorthand. In function parameters, it is syntactic sugar for a generic with a trait bound. In return position, it lets you return a concrete type without naming it:

// In parameter position: shorthand for generics
fn notify(item: &impl std::fmt::Display) {
    println!("Breaking news: {}", item);
}

// In return position: return some type that implements the trait
fn make_greeting(name: &str) -> impl std::fmt::Display {
    format!("Hello, {}! Welcome to Rust.", name)
}

fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

fn main() {
    notify(&"Rust is awesome");
    notify(&42);

    let greeting = make_greeting("Alice");
    println!("{}", greeting);

    let add_five = make_adder(5);
    println!("5 + 3 = {}", add_five(3));
    println!("5 + 10 = {}", add_five(10));
}

Generic Structs and Implementations

Structs can be generic over one or more type parameters. You can then write implementations that apply to all types or only to specific ones:

#[derive(Debug)]
struct Pair<T> {
    first: T,
    second: T,
}

impl<T> Pair<T> {
    fn new(first: T, second: T) -> Self {
        Pair { first, second }
    }

    fn into_tuple(self) -> (T, T) {
        (self.first, self.second)
    }
}

// This impl block only applies when T implements Display + PartialOrd
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
    fn larger(&self) -> &T {
        if self.first >= self.second {
            &self.first
        } else {
            &self.second
        }
    }

    fn display(&self) {
        println!("({}, {})", self.first, self.second);
    }
}

fn main() {
    let int_pair = Pair::new(10, 20);
    int_pair.display();
    println!("Larger: {}", int_pair.larger());

    let str_pair = Pair::new("hello", "world");
    str_pair.display();
    println!("Larger: {}", str_pair.larger());

    let (a, b) = Pair::new(3.14, 2.71).into_tuple();
    println!("Unpacked: {} and {}", a, b);
}

Trait Objects for Dynamic Dispatch

When you need a collection of different types that share a trait, use trait objects with dyn. This uses dynamic dispatch (a vtable lookup at runtime) instead of static dispatch:

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

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

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

impl Drawable for Rectangle {
    fn draw(&self) {
        println!("Drawing rectangle {}x{}", self.width, self.height);
    }
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

fn print_total_area(shapes: &[Box<dyn Drawable>]) {
    let total: f64 = shapes.iter().map(|s| s.area()).sum();
    println!("Total area: {:.2}", total);
}

fn main() {
    let shapes: Vec<Box<dyn Drawable>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Rectangle { width: 4.0, height: 6.0 }),
        Box::new(Circle { radius: 3.0 }),
    ];

    for shape in &shapes {
        shape.draw();
        println!("  Area: {:.2}", shape.area());
    }

    print_total_area(&shapes);
}

Try It Yourself

Create a trait-based system and experiment with adding new types that implement the trait:

use std::fmt;

trait Animal: fmt::Display {
    fn name(&self) -> &str;
    fn sound(&self) -> &str;
    fn legs(&self) -> u32;

    fn describe(&self) {
        println!(
            "{} says '{}' and walks on {} legs",
            self.name(),
            self.sound(),
            self.legs()
        );
    }
}

struct Dog { name: String }
struct Cat { name: String }
struct Spider { name: String }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> &str { "Woof!" }
    fn legs(&self) -> u32 { 4 }
}

impl Animal for Cat {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> &str { "Meow!" }
    fn legs(&self) -> u32 { 4 }
}

impl Animal for Spider {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> &str { "..." }
    fn legs(&self) -> u32 { 8 }
}

impl fmt::Display for Dog {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Dog({})", self.name) }
}
impl fmt::Display for Cat {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Cat({})", self.name) }
}
impl fmt::Display for Spider {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Spider({})", self.name) }
}

fn most_legs(animals: &[Box<dyn Animal>]) -> &dyn Animal {
    animals.iter()
        .max_by_key(|a| a.legs())
        .map(|a| a.as_ref())
        .unwrap()
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog { name: String::from("Rex") }),
        Box::new(Cat { name: String::from("Whiskers") }),
        Box::new(Spider { name: String::from("Charlotte") }),
    ];

    for animal in &animals {
        animal.describe();
    }

    let winner = most_legs(&animals);
    println!("\nMost legs: {} with {} legs", winner, winner.legs());
}

Key Takeaways

  • Traits define shared behavior as a set of methods that types must implement
  • Default method implementations reduce boilerplate when many types share common logic
  • Generics let you write one function or struct that works with many types, with zero runtime cost
  • Trait bounds constrain generics so you can only use types that provide the required behavior
  • impl Trait provides convenient shorthand in both parameter and return position
  • Use dyn Trait with Box for dynamic dispatch when you need collections of mixed types
  • Conditional implementations let you add methods only when type parameters satisfy certain bounds

Pro Tip: Prefer static dispatch (generics) when performance matters and you know the types at compile time. Use dynamic dispatch (dyn Trait) when you need a heterogeneous collection or want to reduce compile times in large codebases.

Next Steps

Traits and generics are the foundation for one of Rust's most elegant abstractions: iterators. Next, we'll explore how the Iterator trait and its adapter methods let you process sequences of data with concise, zero-cost chains.

Next lesson

Iterators

Master Rust's powerful iterator system to write expressive, efficient, and functional-style code

25 min