Skip to main content
Technology & EngineeringRust217 lines

Traits Generics

Rust traits and generics for polymorphism, abstraction, and zero-cost generic programming

Quick Summary26 lines
You are an expert in Rust traits and generics for building flexible, reusable abstractions with zero-cost polymorphism.

## Key Points

- Prefer static dispatch (generics) for performance-critical paths; use `dyn Trait` when you need heterogeneous collections or to reduce binary size.
- Use associated types for traits where each type has exactly one implementation (like `Iterator::Item`).
- Keep trait definitions small and focused — prefer composing multiple small traits over one large one.
- Derive common traits (`Debug`, `Clone`, `PartialEq`, `Eq`, `Hash`) whenever possible.
- Use `where` clauses for readability when bounds get complex.
- Implement `From` / `Into` for type conversions rather than ad-hoc methods.
- **Orphan rule**: You cannot implement a foreign trait on a foreign type. Use the newtype pattern as a workaround.
- **Monomorphization bloat**: Heavy use of generics can increase binary size since each instantiation generates separate code. Consider `dyn Trait` or extracting non-generic inner functions.
- **Overly broad bounds**: Adding too many trait bounds to generics makes APIs rigid. Only require what the function actually uses.
- **Forgetting `+ Send + Sync`**: Trait objects used across threads need explicit `dyn Trait + Send + Sync` bounds.

## Quick Example

```rust
trait PrettyPrint: std::fmt::Display {
    fn pretty(&self) -> String {
        format!("[ {} ]", self)
    }
}
```
skilldb get rust-skills/Traits GenericsFull skill: 217 lines
Paste into your CLAUDE.md or agent config

Traits & Generics — Rust Programming

You are an expert in Rust traits and generics for building flexible, reusable abstractions with zero-cost polymorphism.

Core Philosophy

Traits and generics are Rust's answer to polymorphism, and their design reflects a strong opinion: abstraction should not cost performance. Generics are monomorphized at compile time, producing specialized machine code for each concrete type. This means you get the flexibility of interfaces with the speed of hand-written specialized code. The trade-off is compile time and binary size, but at runtime there is zero indirection.

The trait system encodes behavior contracts in the type system. When a function requires T: Display + Clone, it documents exactly what capabilities it needs -- no more, no less. This is more precise than class hierarchies because traits compose orthogonally. A type can implement any combination of traits without fitting into a single inheritance tree, and each trait implementation is independently verifiable.

Dynamic dispatch via dyn Trait exists for the cases where you genuinely need heterogeneous collections or want to reduce code generation. The choice between impl Trait (static) and dyn Trait (dynamic) should be deliberate: static dispatch for hot paths and known types, dynamic dispatch for plugin systems, collections of mixed types, and reducing monomorphization bloat. Making this choice explicit -- rather than defaulting to virtual dispatch as in most OOP languages -- is a core part of Rust's performance-by-design philosophy.

Anti-Patterns

  • Overly broad trait bounds: Adding T: Clone + Debug + Send + Sync + 'static to every generic function when only Clone is actually used makes APIs rigid and hard to call. Require only the bounds the function body actually exercises.
  • Reaching for dyn Trait by default: Coming from OOP languages, it is tempting to use trait objects everywhere. But dyn Trait incurs heap allocation and vtable indirection. Prefer generics for known-type scenarios and reserve dyn Trait for genuinely heterogeneous collections.
  • Defining one large trait instead of several small ones: A trait with 15 methods forces every implementor to provide all of them. Split into focused traits (Read, Write, Seek) that implementors can adopt independently, and use supertraits to compose them when needed.
  • Implementing foreign traits on foreign types without newtype: The orphan rule exists to prevent conflicting implementations across crates. Attempting to work around it with blanket impls or feature flags leads to fragile, incompatible code. Use the newtype pattern to wrap foreign types cleanly.
  • Ignoring monomorphization bloat in generic-heavy libraries: Each unique instantiation of a generic function generates separate machine code. A generic function called with 20 different types produces 20 copies. For large generic functions, extract the non-generic core into a helper that operates on dyn Trait or concrete types to reduce binary size.

Overview

Traits define shared behavior (like interfaces) and generics allow writing code that works across multiple types. Together they form Rust's primary abstraction mechanism. Rust monomorphizes generic code at compile time, producing specialized machine code with no runtime overhead.

Core Concepts

Defining and Implementing Traits

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

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

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

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

Generic Functions with Trait Bounds

// Bound syntax
fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

// where clause (cleaner for multiple bounds)
fn process<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,
    U: std::fmt::Display,
{
    format!("{} — {u}", t.summarize())
}

impl Trait (Argument Position and Return Position)

// Argument position: syntactic sugar for a generic bound
fn notify(item: &impl Summary) {
    println!("Breaking: {}", item.summarize());
}

// Return position: returns some concrete type implementing the trait
fn make_summary() -> impl Summary {
    Article {
        title: "News".into(),
        body: "Something happened today in the world.".into(),
    }
}

Trait Objects (Dynamic Dispatch)

// dyn Trait behind a pointer for runtime polymorphism
fn print_all(items: &[&dyn Summary]) {
    for item in items {
        println!("{}", item.summarize());
    }
}

// Boxed trait objects for owned, heap-allocated polymorphism
fn create_items() -> Vec<Box<dyn Summary>> {
    vec![
        Box::new(Article { title: "A".into(), body: "Body A content here...".into() }),
    ]
}

Supertraits

trait PrettyPrint: std::fmt::Display {
    fn pretty(&self) -> String {
        format!("[ {} ]", self)
    }
}

Implementation Patterns

Generic Structs

struct Pair<T> {
    first: T,
    second: T,
}

impl<T: PartialOrd + std::fmt::Display> Pair<T> {
    fn larger(&self) -> &T {
        if self.first >= self.second { &self.first } else { &self.second }
    }
}

Associated Types vs. Generic Parameters

Use associated types when there is one natural type per implementation:

trait Iterator {
    type Item; // associated type — one Item per iterator
    fn next(&mut self) -> Option<Self::Item>;
}

// vs. generic param — multiple implementations possible
trait ConvertTo<T> {
    fn convert(&self) -> T;
}

Extension Traits

Add methods to foreign types via a new trait:

trait StringExt {
    fn truncate_to(&self, max_len: usize) -> &str;
}

impl StringExt for str {
    fn truncate_to(&self, max_len: usize) -> &str {
        if self.len() <= max_len {
            self
        } else {
            &self[..self.floor_char_boundary(max_len)]
        }
    }
}

Blanket Implementations

trait Greet {
    fn greet(&self) -> String;
}

// Implement for anything that implements Display
impl<T: std::fmt::Display> Greet for T {
    fn greet(&self) -> String {
        format!("Hello, {self}!")
    }
}

The Newtype Pattern

Implement foreign traits on foreign types by wrapping:

struct Meters(f64);

impl std::fmt::Display for Meters {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:.2}m", self.0)
    }
}

Best Practices

  • Prefer static dispatch (generics) for performance-critical paths; use dyn Trait when you need heterogeneous collections or to reduce binary size.
  • Use associated types for traits where each type has exactly one implementation (like Iterator::Item).
  • Keep trait definitions small and focused — prefer composing multiple small traits over one large one.
  • Derive common traits (Debug, Clone, PartialEq, Eq, Hash) whenever possible.
  • Use where clauses for readability when bounds get complex.
  • Implement From / Into for type conversions rather than ad-hoc methods.

Common Pitfalls

  • Trait object safety: Traits with generic methods or methods returning Self are not object-safe and cannot be used as dyn Trait. Workarounds include using associated types or Box<dyn Trait> return types.
  • Orphan rule: You cannot implement a foreign trait on a foreign type. Use the newtype pattern as a workaround.
  • Monomorphization bloat: Heavy use of generics can increase binary size since each instantiation generates separate code. Consider dyn Trait or extracting non-generic inner functions.
  • Overly broad bounds: Adding too many trait bounds to generics makes APIs rigid. Only require what the function actually uses.
  • Forgetting + Send + Sync: Trait objects used across threads need explicit dyn Trait + Send + Sync bounds.

Install this skill directly: skilldb add rust-skills

Get CLI access →