Step 08 of 17

Traits & generics

Define shared behaviour with traits, write generic code with bounds, and choose between impl Trait, generics and dyn Trait.

19 min 8 sections

Traits are Rust's way of describing what a type can do, and generics let you write code once for every type that can do it. Together they replace interfaces, abstract classes and templates from other languages, with one important twist: by default, generic code is compiled separately for each concrete type, so abstraction costs nothing at runtime. This lesson covers defining and implementing traits, generic functions and structs, trait bounds, the standard traits you'll derive and implement constantly, and when to use dynamic dispatch with dyn Trait instead.

Defining and implementing traits

A trait declares methods a type must provide. It can also provide default implementations:

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

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.author()) // default, uses the required method
    }
}

struct Post {
    title: String,
    author: String,
}

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

impl Summary for Post {
    fn author(&self) -> String {
        self.author.clone()
    }
    fn summarize(&self) -> String {
        format!("{}, by {}", self.title, self.author) // overrides the default
    }
}

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

fn main() {
    let p = Post { title: "Ownership explained".into(), author: "Ana".into() };
    let t = Tweet { username: "ferris".into(), text: "hi".into() };
    println!("{}\n{} ({} chars)", p.summarize(), t.summarize(), t.text.len());
}

Unlike Go's implicit interfaces, implementation is explicit: you write impl Summary for Post. That makes it searchable, and it means a type can't satisfy a trait by accident just because a method name matches.

You can implement your own traits for any type, including standard ones like i32 or Vec<T>, and standard traits for your own types. What you can't do is implement a foreign trait for a foreign type (say, Display for Vec<u8>). This orphan rule guarantees that two crates can never provide conflicting implementations. The workaround is a newtype: struct Hex(Vec<u8>); and impl Display for Hex.

Generic functions and trait bounds

A generic function takes type parameters in angle brackets. Trait bounds say what those types must be able to do:

rust
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
    let mut best = items.first()?;
    for item in items {
        if item > best {
            best = item;
        }
    }
    Some(best)
}

fn main() {
    println!("{:?}", largest(&[3, 9, 2]));          // Some(9)
    println!("{:?}", largest(&["kiwi", "apple"]));  // Some("kiwi")
    println!("{:?}", largest::<f64>(&[]));          // None
}

Without T: PartialOrd, the compiler rejects item > best: it type-checks the generic body once, against the bounds, not against each caller's type. That's why generic errors in Rust show up where you write the function, not deep inside a caller's instantiation as with C++ templates.

Bounds get long, so there are three ways to write them:

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

// 1. Inline
fn show_a<T: Display + Clone>(x: T) -> String { format!("{x}") }

// 2. where clause: easier to read with several parameters
fn show_b<T, U>(t: T, u: U) -> String
where
    T: Display + Clone,
    U: Debug,
{
    format!("{t} {u:?}")
}

// 3. impl Trait in argument position: shorthand for an anonymous generic
fn show_c(x: impl Display) -> String { format!("{x}") }

Monomorphization

When you call largest with &[i32] and &[&str], the compiler generates two copies of the function, each specialized for its type. This is monomorphization. The result is as fast as hand-written code for each type, with full inlining. The cost is compile time and binary size, which grow with the number of distinct instantiations.

Generic structs and impl blocks

Structs and enums can be generic too; you've already used Vec<T>, Option<T> and Result<T, E>.

rust
#[derive(Debug)]
struct Pair<T> {
    a: T,
    b: T,
}

impl<T> Pair<T> {
    fn new(a: T, b: T) -> Self {
        Self { a, b }
    }
}

// Methods that only exist when T supports comparison and printing.
impl<T: PartialOrd + std::fmt::Display> Pair<T> {
    fn print_max(&self) {
        let max = if self.a >= self.b { &self.a } else { &self.b };
        println!("max is {max}");
    }
}

fn main() {
    Pair::new(3, 8).print_max();
    let p = Pair::new(vec![1], vec![2]); // fine: new has no bounds
    println!("{p:?}");                    // but p.print_max() wouldn't compile: Vec isn't Display
}

You can also implement a trait for every type that satisfies a bound. This is a blanket implementation, and the standard library uses it heavily: impl<T: Display> ToString for T is why every displayable type has .to_string().

Associated types

Some traits have a type that belongs to the implementation rather than being chosen by the caller. Iterator is the classic case:

rust
struct Countdown(u32);

impl Iterator for Countdown {
    type Item = u32; // each implementation picks exactly one Item type

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

fn main() {
    let v: Vec<u32> = Countdown(3).collect();
    println!("{v:?}"); // [3, 2, 1]
}

Implementing one method, next, gives Countdown every iterator adapter for free: map, filter, sum, collect and the rest come from default methods on the trait.

Use an associated type when there's one natural choice per implementing type (an iterator yields one kind of item). Use a generic parameter (trait From<T>) when a type can implement the trait many times for different Ts.

The standard traits

You'll derive or implement these constantly:

TraitGives youUsually
Debug{:?} formatting#[derive] on nearly everything
Clone / Copy.clone() / implicit copiesDerive; Copy only for small plain data
PartialEq / Eq== and !=Derive; Eq when equality is total (not floats)
PartialOrd / Ord<, >, sortingDerive, or implement for custom orderings
HashUse as a HashMap keyDerive together with Eq
DefaultT::default(), ..Default::default()Derive, or implement for meaningful defaults
Display{} formatting, .to_string()Implement by hand for user-facing text
From / IntoConversions; ? uses FromImplement From; Into comes free
DropCleanup on scope endImplement for resource owners
rust
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}

impl std::fmt::Display for Version {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
    }
}

fn main() {
    let mut vs = vec![
        Version { major: 1, minor: 10, patch: 0 },
        Version { major: 1, minor: 2, patch: 7 },
        Version::default(),
    ];
    vs.sort(); // derived Ord compares fields in declaration order
    let names: Vec<String> = vs.iter().map(|v| v.to_string()).collect();
    println!("{names:?}"); // ["v0.0.0", "v1.2.7", "v1.10.0"]
}

Dynamic dispatch with dyn Trait

Generics need every type known at compile time. Sometimes you need a collection of different types that share a trait, or you want to choose an implementation at runtime. That's a trait object, dyn Trait, always behind a pointer such as Box<dyn Trait> or &dyn Trait:

rust
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle(f64);
struct Square(f64);

impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
    fn name(&self) -> &str { "circle" }
}

impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
    fn name(&self) -> &str { "square" }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Square(2.0))];
    for s in &shapes {
        println!("{} {:.2}", s.name(), s.area());
    }
    let total: f64 = shapes.iter().map(|s| s.area()).sum();
    println!("{total:.2}");
}

A Box<dyn Shape> is a fat pointer: a pointer to the data plus a pointer to a vtable of the concrete type's methods. Each call looks up the method in the vtable at runtime. That's what Java, C# and Go interfaces do for every call.

Generics (T: Trait, impl Trait)Trait objects (dyn Trait)
DispatchStatic: resolved at compile time, inlinableDynamic: vtable lookup per call
Mixed types in one collectionNoYes
Binary sizeOne copy per type usedOne copy
Runtime costNoneAn indirect call; blocks inlining
RestrictionsNoneTrait must be dyn compatible (no generic methods, no Self returns)

Default to generics. Reach for dyn Trait when you need heterogeneous collections, plugin-style registries, or to keep compile times and binary size down for code that isn't hot. The static vs dynamic dispatch concept covers the trade-off in more depth.

impl Trait in return position

-> impl Trait returns some concrete type the caller doesn't get to name. It's still static dispatch, and it's how you return closures and iterator chains whose real types are unspeakable:

rust
fn evens(limit: u32) -> impl Iterator<Item = u32> {
    (0..limit).filter(|n| n % 2 == 0)
}

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

fn main() {
    println!("{:?}", evens(10).collect::<Vec<_>>());
    println!("{}", make_adder(5)(10));
}

The function must return a single concrete type on every path. If different branches return different types, you need Box<dyn Trait>.

Practice

Build a tiny notification system.

  1. Define trait Notifier { fn send(&self, to: &str, msg: &str) -> Result<(), String>; fn name(&self) -> &'static str { "notifier" } }.
  2. Implement it for struct Email { from: String } and struct Sms { sender_id: u32 }, overriding name.
  3. Write a generic function fn notify_all<N: Notifier>(n: &N, users: &[&str], msg: &str) -> usize that returns how many sends succeeded. Make Sms fail for any number that doesn't start with +.
  4. Write a struct Dispatcher { channels: Vec<Box<dyn Notifier>> } with fn broadcast(&self, to: &str, msg: &str) that tries every channel and prints the result for each.
  5. Explain why notify_all can't be used to store both notifiers in one Vec, and why Dispatcher can.
Show solution
rust
trait Notifier {
    fn send(&self, to: &str, msg: &str) -> Result<(), String>;
    fn name(&self) -> &'static str {
        "notifier"
    }
}

struct Email {
    from: String,
}

struct Sms {
    sender_id: u32,
}

impl Notifier for Email {
    fn send(&self, to: &str, msg: &str) -> Result<(), String> {
        println!("email {} -> {to}: {msg}", self.from);
        Ok(())
    }
    fn name(&self) -> &'static str {
        "email"
    }
}

impl Notifier for Sms {
    fn send(&self, to: &str, msg: &str) -> Result<(), String> {
        if !to.starts_with('+') {
            return Err(format!("{to} is not a phone number"));
        }
        println!("sms #{} -> {to}: {msg}", self.sender_id);
        Ok(())
    }
    fn name(&self) -> &'static str {
        "sms"
    }
}

fn notify_all<N: Notifier>(n: &N, users: &[&str], msg: &str) -> usize {
    users.iter().filter(|u| n.send(u, msg).is_ok()).count()
}

struct Dispatcher {
    channels: Vec<Box<dyn Notifier>>,
}

impl Dispatcher {
    fn broadcast(&self, to: &str, msg: &str) {
        for ch in &self.channels {
            match ch.send(to, msg) {
                Ok(()) => println!("[{}] sent", ch.name()),
                Err(e) => println!("[{}] failed: {e}", ch.name()),
            }
        }
    }
}

fn main() {
    let sms = Sms { sender_id: 7 };
    println!("{}", notify_all(&sms, &["+4512345678", "[email protected]"], "hi")); // 1

    let d = Dispatcher {
        channels: vec![Box::new(Email { from: "[email protected]".into() }), Box::new(sms)],
    };
    d.broadcast("+4512345678", "deploy finished");
}

notify_all::<Sms> and notify_all::<Email> are two separate monomorphized functions; each call works with exactly one concrete type, so it can't hold a mix. Vec<Box<dyn Notifier>> erases the concrete type behind a vtable pointer, so each element can be a different type and calls are dispatched at runtime.

Key takeaways

  • A trait lists methods a type must implement, optionally with defaults; implementations are explicit, and the orphan rule prevents conflicts between crates.
  • Generic code is checked against its bounds once and monomorphized per type: zero runtime cost, more compile time and binary size.
  • Use associated types when each implementation has one natural choice (like Iterator::Item).
  • Derive the standard traits (Debug, Clone, PartialEq, Hash, Default…) and implement Display and From by hand.
  • Prefer generics and impl Trait; use Box<dyn Trait> for heterogeneous collections and runtime choice, at the cost of a vtable call.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.Why does fn max<T>(a: T, b: T) -> T { if a > b { a } else { b } } fail to compile?

  2. 2.You need a Vec holding both FileLogger and HttpLogger values that implement Logger. Which type works?

  3. 3.What does monomorphization mean for a generic function called with three different types?

  4. 4.Why can't you write impl std::fmt::Display for Vec<u8> in your crate?

Concepts in this lesson

One-page notes on each idea, for review or a quick lookup later.

Finished reading?

Mark it complete to fill in your roadmap.