Traits

Traits define shared behavior. They’re similar to interfaces in other languages, but with default implementations, zero-cost static dispatch, and powerful composition.

Defining and Implementing Traits

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

    // Default implementation - implementors can override or use as-is
    fn preview(&self) -> String {
        format!("{}...", &self.summarize()[..20])
    }
}

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

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, &self.body[..50])
    }
    // preview() uses the default implementation
}

Trait Bounds

Constrain generic types to require certain behavior.

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

// Shorthand with impl Trait (preferred for simple cases)
fn notify(item: &impl Summary) {
    println!("Breaking: {}", item.summarize());
}

// Multiple bounds
fn process<T: Summary + Clone>(item: &T) {
    let copy = item.clone();
    println!("{}", copy.summarize());
}

// Where clause for complex bounds
fn compare<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,
    U: Summary + std::fmt::Debug,
{
    format!("{} vs {:?}", t.summarize(), u)
}

Static vs Dynamic Dispatch

Rust gives you a choice between compile-time and runtime polymorphism.

impl Trait - static dispatch (monomorphization)

The compiler generates a separate version of the function for each concrete type. Zero overhead at runtime.

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

// Compiler generates:
// fn print_summary_article(item: &Article) { ... }
// fn print_summary_tweet(item: &Tweet) { ... }

dyn Trait - dynamic dispatch (trait objects)

Uses a vtable for runtime polymorphism. Small overhead, but allows heterogeneous collections.

fn print_summary(item: &dyn Summary) {
    println!("{}", item.summarize());
}

// Store different types in one collection
let items: Vec<Box<dyn Summary>> = vec![
    Box::new(article),
    Box::new(tweet),
];
ApproachWhen to use
impl TraitDefault choice. Faster, optimized per type
dyn TraitHeterogeneous collections, plugin systems, reducing compile times

Gotcha: Not all traits can be made into trait objects. The trait must be “object safe” - no Self in return types, no generic methods. The compiler tells you when this is violated.

Common Standard Library Traits

Derive macros - automatic implementations

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p);           // Debug
let p2 = p.clone();            // Clone
assert_eq!(p, p2);             // PartialEq

Essential traits reference

TraitPurposeDerivable?
DebugFormat with {:?} for logging/debuggingYes
CloneExplicit deep copy via .clone()Yes
CopyImplicit copy on assignment (stack types only)Yes
PartialEq / EqEquality comparison (==)Yes
PartialOrd / OrdOrdering comparison (<, >, sort)Yes
HashHashing for HashMap/HashSet keysYes
DefaultProvide a default valueYes
DisplayFormat with {} for user-facing outputNo
From / IntoType conversionNo
IteratorProduce a sequence of valuesNo

Display - user-facing formatting

use std::fmt;

struct Color(u8, u8, u8);

impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
    }
}

println!("{}", Color(255, 128, 0)); // "#ff8000"

From / Into - type conversion

Implementing From gives you Into for free.

struct Meters(f64);

impl From<f64> for Meters {
    fn from(value: f64) -> Self {
        Meters(value)
    }
}

let m: Meters = 5.0.into();       // Into (free from From impl)
let m = Meters::from(5.0);         // From (explicit)

Tip: Prefer implementing From over Into. You get both conversions, and From is more idiomatic. The ? operator uses From for error conversion.

Iterator

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

impl Iterator for Counter {
    type Item = u32;

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

let sum: u32 = Counter { current: 0, max: 5 }
    .filter(|n| n % 2 == 0)
    .sum();
// sum = 2 + 4 = 6

Supertraits

Require another trait as a prerequisite.

trait Printable: std::fmt::Display {
    fn print(&self) {
        println!("{self}"); // can use Display because it's required
    }
}

Extension Traits

Add methods to types you don’t own by defining a new trait.

trait StrExt {
    fn is_blank(&self) -> bool;
}

impl StrExt for str {
    fn is_blank(&self) -> bool {
        self.trim().is_empty()
    }
}

"  ".is_blank(); // true

Tip: The caller must use the trait to access extension methods. This prevents conflicts between unrelated extensions.

Next: Error Handling | Ownership