Skip to content
Codeloom
Rust

Traits and Generics in Rust

Master Rust traits and generics — defining traits, trait bounds, default methods, trait objects, and generic data structures.

·4 min read · By Codeloom
Intermediate 12 min read

What you'll learn

  • How to define and implement traits
  • Trait bounds and where clauses for generics
  • Static dispatch vs dynamic dispatch (trait objects)
  • Common standard library traits

Prerequisites

  • Rust basics (structs, enums, functions)
  • Understanding of ownership and references

Traits are Rust’s mechanism for shared behavior — similar to interfaces in Go or Java, but with default implementations, associated types, and zero-cost static dispatch.

Defining a trait

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

Implementing a trait

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

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

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

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

Default methods

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

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
    // summarize() uses the default implementation
}

Traits as parameters

impl Trait syntax

fn notify(item: &impl Summary) {
    println!("Breaking: {}", item.summarize());
}

Trait bound syntax

fn notify<T: Summary>(item: &T) {
    println!("Breaking: {}", item.summarize());
}

Multiple bounds

fn notify(item: &(impl Summary + Display)) {
    println!("{}", item);
}

// or with trait bound syntax
fn notify<T: Summary + Display>(item: &T) {
    println!("{}", item);
}

Where clauses

For complex bounds, use where for readability.

fn process<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,
    U: Display + Debug,
{
    format!("{} — {:?}", t.summarize(), u)
}

Returning impl Trait

fn create_summarizable() -> impl Summary {
    Tweet {
        username: "bot".to_string(),
        text: "Hello world".to_string(),
    }
}

The caller does not know the concrete type — only that it implements Summary.

Generics

Generic functions

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

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

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

Generic structs

struct Point<T> {
    x: T,
    y: T,
}

impl<T: Display> Point<T> {
    fn print(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

impl Point<f64> {
    fn distance(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Multiple generic types

struct Pair<A, B> {
    first: A,
    second: B,
}

Trait objects (dynamic dispatch)

When you need to store different types that implement the same trait, use dyn Trait.

fn get_items() -> Vec<Box<dyn Summary>> {
    vec![
        Box::new(Article {
            title: "AI News".to_string(),
            content: "Artificial intelligence is transforming...".to_string(),
        }),
        Box::new(Tweet {
            username: "rustlang".to_string(),
            text: "Rust 2024 edition is here!".to_string(),
        }),
    ]
}

Static vs dynamic dispatch

Featureimpl Trait / genericsdyn Trait
DispatchStatic (monomorphized)Dynamic (vtable)
PerformanceZero-costPointer indirection
Concrete type known atCompile timeRuntime
Can store mixed typesNoYes

Use generics (static dispatch) by default. Use dyn when you need a collection of mixed types or the concrete type is decided at runtime.

Common standard library traits

TraitPurpose
DisplayFormat for users ({})
DebugFormat for developers ({:?})
CloneDeep copy with .clone()
CopyImplicit bit-copy on assignment
PartialEq, EqEquality comparison
PartialOrd, OrdOrdering comparison
HashHash for use in HashMap/HashSet
DefaultProvide a default value
From/IntoType conversions
IteratorIteration protocol

Deriving traits

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    name: String,
    age: u32,
}

Associated types

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {
    count: u32,
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

Summary

Traits define shared behavior. Generics with trait bounds give you static dispatch and zero-cost abstractions. Trait objects (dyn Trait) give you dynamic dispatch when you need runtime polymorphism. Start with generics, reach for trait objects when the concrete type cannot be known at compile time, and derive standard traits to get common functionality for free.