Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Errors, Collections, Traits and Concurrency

Traits and Generics

Traits define shared behaviour (like interfaces); generics write code once for many types; trait bounds say what a generic type must be able to do. Implement Display, use impl Trait and dyn Trait.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A notification system where emails and SMS messages share a Notify trait, a generic function that works with any type implementing it, and a list holding different kinds of notifiers.

The idea

A trait declares methods that types can implement, like an interface:

trait Notify {
    fn send(&self, message: &str) -> String;
    fn channel(&self) -> &str { "generic" }   // a default method
}

Generics let one function or struct work with many types: fn largest<T: PartialOrd>(items: &[T]) -> &T. The trait bound (T: PartialOrd) says which abilities T must have. Rust generates a specialised copy per type (static dispatch), so generics cost nothing at runtime.

Ways to accept "anything that implements a trait":

  • fn alert(n: &impl Notify): short syntax for a generic parameter (static dispatch).
  • fn alert<N: Notify>(n: &N) or with where N: Notify: the explicit form.
  • Vec<Box<dyn Notify>>: a trait object for mixing different types in one collection (dynamic dispatch through a vtable).

Standard traits you'll implement often: Display (for {}), Debug, Clone, PartialEq, Default, From, Iterator.

The code

A trait, generic functions and trait objects
use std::fmt;

trait Notify {
    fn send(&self, message: &str) -> String;
    fn channel(&self) -> &str {
        "generic" // default implementation
    }
}

struct Email { to: String }
struct Sms { number: String }

impl Notify for Email {
    fn send(&self, message: &str) -> String {
        format!("email to {}: {}", self.to, message)
    }
    fn channel(&self) -> &str { "email" }
}

impl Notify for Sms {
    fn send(&self, message: &str) -> String {
        let short: String = message.chars().take(20).collect(); // SMS gets a short version
        format!("sms to {}: {}", self.number, short)
    }
}

fn alert(n: &impl Notify, message: &str) -> String {
    format!("[{}] {}", n.channel(), n.send(message))
}

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

struct Money(i64); // cents

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "${}.{:02}", self.0 / 100, self.0 % 100)
    }
}

fn main() {
    let email = Email { to: "[email protected]".into() };
    let sms = Sms { number: "+44 7700 900123".into() };
    println!("{}", alert(&email, "Your order has shipped"));
    println!("{}", alert(&sms, "Your order has shipped and arrives Friday"));

    let notifiers: Vec<Box<dyn Notify>> = vec![Box::new(email), Box::new(sms)]; // mixed types
    for n in &notifiers {
        println!("{} ready", n.channel());
    }

    println!("largest number: {}", largest(&[3, 9, 4]));
    println!("largest word: {}", largest(&["pear", "apple", "zucchini"]));
    println!("price: {}", Money(129_905));
}

Output

[email] email to [email protected]: Your order has shipped
[generic] sms to +44 7700 900123: Your order has shipp
email ready
generic ready
largest number: 9
largest word: zucchini
price: $1299.05

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Reaching for dyn Trait by default. Generics (impl Trait) are faster and simpler; use trait objects when you need mixed types in one collection or to shrink compile times.
  • Missing trait bounds. largest can't compare items unless T: PartialOrd; the compiler error says which bound to add.
  • Implementing foreign traits on foreign types (e.g. Display for Vec<T>) isn't allowed (the orphan rule); wrap the type in your own struct ("newtype"), like Money here.
  • Overly generic code that's hard to read; start concrete, generalise when there's a second use.

Key points

  • Traits define shared behaviour, with optional default methods.
  • Generics + trait bounds: one implementation, specialised per type at compile time.
  • impl Trait (static) vs Box<dyn Trait> (dynamic, mixed collections).
  • Implement standard traits like Display to integrate with the language.

Further reading: Generic types, traits, and lifetimes, Trait objects.

Practice questions

What is the difference between `impl Trait` and `dyn Trait`?

Answer

impl Trait (or a generic with a trait bound) uses static dispatch: the compiler generates code for each concrete type, which is fast. dyn Trait is a trait object using dynamic dispatch through a vtable, allowing different concrete types behind one pointer (e.g. Vec<Box<dyn Trait>>), at a small runtime cost.

What is a trait bound?

Answer

A requirement on a generic type parameter, such as T: PartialOrd or where T: Display + Clone, stating which traits the type must implement so the generic code can use those methods.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?