Article · 1 min read

16 — Traits and Generics

Traits are Rust's answer to interfaces/typeclasses — they define shared behavior. Generics parametrize code over types. Together they're the foundation of Rust's abstraction.

Defining and Implementing Traits

trait Greet {
    fn say_hi(&self) -> String;
    fn say_loud(&self) -> String {
        format!("{}!!!", self.say_hi())    // default method body
    }
}

struct User { name: String }
impl Greet for User {
    fn say_hi(&self) -> String { format!("hi {}", self.name) }
}
  • Default methods can be overridden.
  • Implementations are explicit (no automatic interface implementation like Java).
  • You can implement a trait for a type only if either the trait or the type is local to your crate (the orphan rule) — prevents conflicting impls across crates.

Trait Objects vs Static Dispatch

fn print_all<T: Greet>(items: &[T]) { /* monomorphized per T */ }
fn print_dyn(items: &[Box<dyn Greet>]) { /* dynamic dispatch */ }
  • Generics + trait bounds = static dispatch (inlined, zero-cost, code duplication per type).
  • dyn Trait = dynamic dispatch via vtable (one copy, indirect call, slightly slower, enables heterogeneous collections).

Trait Object Requirements (Object Safety)

A trait is object-safe iff:

  • No associated functions / methods returning Self (by value).
  • No generics in methods.
  • All methods take self by reference (or have where Self: Sized).
  • Self: Sized super-bound disqualifies.

Clone/Iterator/PartialEq aren't object-safe. Greet, Display, Debug are.

let v: Vec<Box<dyn Greet>> = vec![Box::new(User { name: "a".into() })];

Default Type Parameters and Associated Types

Generics vs Associated Types

// Generic trait — caller picks T:
trait Container<T> { fn item(&self) -> &T; }

// Associated typeimpl picks the type:
trait Container { type Item; fn item(&self) -> &Self::Item; }

Use associated types when each type has one natural inner type (e.g., Iterator::Item). Use generics when the type can carry multiple variants (e.g., From<T>).

Default Associated Type

trait Rng { type Output = u64; fn next(&self) -> Self::Output; }

Trait Bounds

fn max<T: PartialOrd + Copy>(a: T, b: T) -> T { if a > b { a } else { b } }

fn sum_all<T>(items: &[T]) -> T
where
    T: Sum + Copy,
{
    items.iter().copied().sum()
}

where clauses are more readable for long bounds and enable more expressiveness (bounds on associated types, lifetimes).

impl Trait

In argument position

fn print(it: impl Iterator<Item = i32>) { /* ... */ }
// equivalent to:
fn print<T: Iterator<Item = i32>>(it: T) { /* ... */ }

In return position

fn counter() -> impl Iterator<Item = u32> {
    (0..5).map(|x| x * 2)
}
  • Returns some concrete type that implements the trait — the actual type is hidden from the caller.
  • Cannot be conditional (no if cond { type A } else { type B }).
  • Each return-site must use a single concrete type.
  • For returning different types, use Box<dyn Trait> or trait objects.

Common Standard Traits

TraitPurpose
DisplayUser-facing string ({})
DebugDeveloper string ({:?})
Clone, CopyDuplication
PartialEq, EqEquality
PartialOrd, OrdOrdering
HashHashing
DefaultDefault::default()
From, Into, TryFrom, TryIntoConversions
AsRef, AsMutCheap borrows
IteratorIteration
Add, Sub, Mul, DivOperator overloading
Index, IndexMut[]
DropDestructor
SizedHas a known size
Send, SyncThread safety (auto)
Unpin, PinAsync/pinning
Fn, FnMut, FnOnceClosures

From and Into

impl From<i32> for My { fn from(x: i32) -> Self { /* ... */ } }
let m: My = 5i32.into();

Implementing From automatically gives you Into. Idiomatic: implement From, never Into directly.

FromStr is the parsing version (str::parse() uses it).

AsRef and AsMut

fn open<P: AsRef<Path>>(path: P) { let p = path.as_ref(); /* p: &Path */ }
open("file.txt");           // &str: AsRef<Path>
open(Path::new("f"));       // &Path: AsRef<Path>
open(String::from("f"));    // String: AsRef<Path>

Multi-source APIs use AsRef<T> to accept &str, String, &Path, &OsStr, etc.

Operator Overloading

use std::ops::Add;
struct Vec2 { x: f64, y: f64 }
impl Add for Vec2 {
    type Output = Vec2;
    fn add(self, rhs: Vec2) -> Vec2 { Vec2 { x: self.x + rhs.x, y: self.y + rhs.y } }
}
let v = Vec2 { x: 1.0, y: 0.0 } + Vec2 { x: 0.0, y: 1.0 };

You can overload Add, Sub, Mul, Div, Rem, Neg, Index, IndexMut, Deref, DerefMut, BitAnd, BitOr, Shl, Shr, Fn*, etc.

Deref Coercion

impl Deref for My { type Target = Inner; fn deref(&self) -> &Inner { &self.inner } }
let m = My { inner: Inner { x: 5 } };
let x = m.x;     // m.x works via Deref coercion

String: Deref<Target = str>, Vec<T>: Deref<Target = [T]>, Box<T>: Deref<Target = T>. This enables method/field forwarding and &-coercions.

Don't abuse Deref for inheritance — it's a memory-layout mechanism, not a modeling tool.

Drop

impl Drop for File {
    fn drop(&mut self) {
        // close file, free resources
    }
}

Runs automatically at scope end. Don't call directly — use std::mem::drop(value) to drop early.

Supertraits

trait Pretty: Debug { fn pretty(&self) { /* can use {:?} */ } }

A supertrait bound means "any type implementing Pretty must also implement Debug".

Trait Composition

trait Read: io::Read + BufRead {}
impl<T: io::Read + BufRead> Read for T {}

Blanket impl gives any type with both underlying traits the composite trait.

Blanket Implementations

impl<T: Display> ToString for T {
    fn to_string(&self) -> String { /* ... */ }
}

A blanket impl covers all matching types. Powerful but can lock out other impls (orphan-rule implications).

Traits with Const Generics

trait Bytes<const N: usize> { fn data(&self) -> [u8; N]; }

Marker Traits

Zero-method traits that tag types: Sized, Send, Sync, Unpin, Copy. Some are auto-traits (compiler-implemented when possible).

Sealed Traits

To prevent downstream impls while still exposing a stable API:

mod private { pub trait Sealed {} }
pub trait Public: private::Sealed { /* ... */ }

Downstream types can't implement Sealed, so they can't implement Public. Used by std and many crates for forward compatibility.

Edge Cases & Pitfalls

  • Orphan rule: can't implement external trait for external type. Use the newtype pattern to wrap and implement.
  • Self returns break object safety: traits returning Self can't be made into dyn Trait.
  • Method resolution: when multiple traits provide the same method name, you must write Trait::method(&self) or use UFCS.
  • Conflicting impls: blanket impls can cause "conflicting implementations" errors; design carefully.
  • PartialEq vs Eq: Eq is a marker requiring reflexivity; floats lack Eq.
  • Trait objects can't have generic methods at runtime: fn dyn_call<T>(&self, x: T) is forbidden on dyn Trait.
  • impl Trait in argument position is sugar for a generic — not a way to accept trait objects.
  • Self: Sized bound on a method excludes it from the vtable — useful for "static-only" methods on an object-safe trait.
  • Generic method on trait object is impossible — workaround is to expose concrete variants.
  • Lifetime bounds on traits: trait Foo<'a> requires the impl to specify a lifetime; used when methods borrow from inputs.

Summary

Traits define behavior; generics parametrize code; impl Trait is sugar for both. Use trait bounds to require capabilities. Object safety decides whether you can use dyn Trait. Implement From, Display/Debug, and Default for ergonomics. Avoid abusing Deref. Sealed traits give you stable APIs.

Next: Lifetimes in generics + the deeper type-system chapter.