Skip to content

Traits and Generics

Traits are Rust’s answer to interfaces, type classes, and concepts. They define shared behavior that Types can implement. Unlike inheritance, traits are composable — a type can implement any number of Traits.

trait Summary {
fn summarize(&self) -> String;
fn preview(&self) -> String {
let full = self.summarize();
if full.len() > 50 {
format!("{}...", &full[..50])
} else {
full
}
}
}

summarize is a required method — every type implementing Summary must provide it. preview Is a default method — types can override it, but if they do not, the default implementation is Used.

struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
}
struct Tweet {
username: String,
text: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.text)
}
}

You can only implement a trait for a type if either the trait or the type is defined in your crate. You cannot implement Display for Vec<T> (both are from the standard library) in your own crate. This prevents coherence issues — two crates could implement the same trait for the same type with Different behavior.

Workarounds:

  • The newtype pattern: wrap Vec<T> in your own struct and implement the trait on the wrapper
  • Local traits: define your own trait and implement it for Vec<T>
struct MyVec(Vec<i32>);
impl std::fmt::Display for MyVec {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "[{}]", self.0.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", "))
}
}

Default methods can call required methods, allowing the implementing type to provide a single method And get additional behavior for free:

trait Processor {
fn process(&self, data: &str) -> String;
fn process_and_log(&self, data: &str) -> String {
println!("processing: {}", data);
let result = self.process(data);
println!("result: {}", result);
result
}
}
struct ToUpper;
impl Processor for ToUpper {
fn process(&self, data: &str) -> String {
data.to_uppercase()
}
}
let p = ToUpper;
p.process_and_log("hello"); // prints processing/result, returns "HELLO"

Default methods can also call other default methods:

trait Container {
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn is_non_empty(&self) -> bool {
!self.is_empty()
}
}

Trait bounds constrain generic types to those that implement specific traits.

fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// Equivalent with where clause (preferred for complex bounds)
fn print_summary<T>(item: &T)
where
T: Summary,
{
println!("{}", item.summarize());
}
fn compare_and_display<T: std::fmt::Display + PartialOrd>(a: T, b: T) {
if a < b {
println!("{} is less than {}", a, b);
} else {
println!("{} is >= {}", a, b);
}
}
// With where clause (more readable for many bounds)
fn complex_function<T, U>(t: &T, u: &U)
where
T: Display + Clone + Send + 'static,
U: Iterator<Item = T> + Debug,
{
// ...
}
fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: AsRef<str>,
{
println!("announcement: {}", ann.as_ref());
if x.len() > y.len() { x } else { y }
}
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list.iter().skip(1) {
if item > largest {
largest = item;
}
}
largest
}
struct Point<T> {
x: T,
y: T,
}
impl<T: std::ops::Add<Output = T> + Copy> Point<T> {
fn sum(&self) -> T {
self.x + self.y
}
}
let int_point = Point { x: 1, y: 2 };
let float_point = Point { x: 1.0, y: 2.0 };
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}

You can implement methods conditionally based on trait bounds:

struct Wrapper<T>(T);
impl<T: Display> Wrapper<T> {
fn display(&self) {
println!("{}", self.0);
}
}
impl<T: Debug> Wrapper<T> {
fn debug(&self) {
println!("{:?}", self.0);
}
}
enum Maybe<T> {
Just(T),
Nothing,
}
impl<T> Maybe<T> {
fn is_just(&self) -> bool {
matches!(self, Maybe::Just(_))
}
fn is_nothing(&self) -> bool {
matches!(self, Maybe::Nothing)
}
}
impl<T: Clone> Maybe<T> {
fn unwrap_or_clone(&self, default: T) -> T {
match self {
Maybe::Just(v) => v.clone(),
Maybe::Nothing => default,
}
}
}

Rust performs monomorphization — the compiler generates a separate copy of each generic function For every concrete type used. This happens at compile time and produces optimized, specialized code With no runtime overhead.

fn id<T>(x: T) -> T { x }
fn main() {
let a = id(42_i32); // compiler generates: fn id_i32(x: i32) -> i32 { x }
let b = id("hello"); // compiler generates: fn id_str(x: &str) -> &str { x }
let c = id(3.14_f64); // compiler generates: fn id_f64(x: f64) -> f64 { x }
}

The downside: monomorphization increases binary size (code bloat) because each type specialization Produces a separate copy of the function. In practice, this is rarely a problem because LLVM can Merge identical machine code after optimization. When code bloat is a concern (e.g., in embedded Systems), use dynamic dispatch via dyn Trait to share a single implementation.

// Static dispatch (monomorphized) — no vtable, inlinable
fn process<T: Display>(item: T) {
println!("{}", item);
}
// Dynamic dispatch (vtable) — single copy, runtime lookup
fn process_dyn(item: &dyn Display) {
println!("{}", item);
}
PropertyStatic DispatchDynamic Dispatch
OverheadNone (after inlining)Vtable lookup per call
Binary sizeLarger (per type copy)Smaller (single copy)
InliningYesNo (indirect call)
FlexibilityCompile-time onlyRuntime polymorphism
Cache behaviorBetter (direct call)Worse (indirect call)

A trait object &dyn Trait or Box<dyn Trait> is a fat pointer: a pointer to the data plus a Pointer to the vtable. The vtable contains function pointers for each method in the trait.

┌─────────────────────────────────────────┐
│ &dyn Display │
│ ┌──────────────┬────────────────────┐ │
│ │ data ptr │ vtable ptr │ │
│ └──────┬───────┴────────┬───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ String │ │ vtable: │ │
│ │ data │ │ fmt() ─────┼──┐ │
│ └─────────────┘ │ ... │ │ │
│ └─────────────┘ │ │
│ ▼ │
│ ┌────────────┐│
│ │ Display:: ││
│ │ fmt impl ││
│ └────────────┘│
└─────────────────────────────────────────┘
trait Animal {
fn make_sound(&self);
}
struct Dog;
impl Animal for Dog {
fn make_sound(&self) { println!("woof"); }
}
struct Cat;
impl Animal for Cat {
fn make_sound(&self) { println!("meow"); }
}
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for animal in &animals {
animal.make_sound();
}

Not all traits can be used as trait objects. A trait is object safe if:

  1. It does not have any associated const or fn items with type parameters, generic methods, or methods that return Self (except &Self/&mut Self).
  2. It does not have any associated type that uses Self in non-trivial ways.
  3. All methods have a receiver (&self``&mut selfOr self). No associated functions.
// Object-safe
trait Display {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result;
}
// NOT object-safe (generic method)
trait Container {
fn get<T>(&self, index: usize) -> Option<&T>; // generic parameter
}
// NOT object-safe (returns Self)
trait Clone {
fn clone(&self) -> Self; // Self in return position
}

The compiler error message explains why a trait is not object-safe when you try to use dyn Trait With it.

You can upcast trait objects to supertraits:

trait Base {
fn base_method(&self);
}
trait Derived: Base {
fn derived_method(&self);
}
struct MyType;
impl Base for MyType {
fn base_method(&self) { println!("base"); }
}
impl Derived for MyType {
fn derived_method(&self) { println!("derived"); }
}
let derived: Box<dyn Derived> = Box::new(MyType);
let base: Box<dyn Base> = derived; // upcast

A blanket implementation implements a trait for all types that satisfy certain bounds. This is one Of Rust’s most powerful patterns:

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

This means every type implementing Display automatically gets to_string(). You never need to Implement ToString manually — just implement Display.

The standard library has many blanket implementations:

impl<T: Debug> Debug for &T { ... }
impl<T: Display> Display for &T { ... }
impl<T: Clone> Clone for &T { ... }
impl<T: ?Sized> Clone for Box<T> where T: Clone { ... }
impl<T, E> From<E> for Result<T, E> where T: From<E> { ... }
trait ScalarOps: Copy + std::ops::Add<Output = Self> + std::ops::Mul<Output = Self> {
fn squared(self) -> Self {
self * self
}
fn sum_with(self, other: Self) -> Self {
self + other
}
}
// Blanket impl — every type that satisfies the bounds gets these methods
impl<T> ScalarOps for T
where
T: Copy + std::ops::Add<Output = T> + std::ops::Mul<Output = T>,
{
}

Marker traits have no methods — they exist purely as compile-time markers of capabilities.

A type is Send if it is safe to transfer ownership to another thread. Most types are Send by Default. Types containing raw pointers, RcOr non-thread-safe interior mutability are not Send.

A type is Sync if it is safe to share references to it between threads (i.e., &T is Send). Most types are Sync by default. Rc<T> is not Sync because cloning an Rc from multiple Threads would create a data race on the reference count.

A type is Copy if it can be duplicated by a bitwise copy. It is automatically implemented for Types where all fields are Copy. Types with destructors (Drop) cannot be Copy.

Clone explicitly defines how to create a deep copy. Unlike Copy``Clone may involve heap Allocation or other non-trivial operations.

All generic type parameters have an implicit Sized bound by default. This means fn foo<T>(x: T) Requires T to have a known size at compile time. Use T: ?Sized to relax this:

fn print_len<T: ?Sized>(value: &T)
where
T: Display,
{
println!("{}", value);
}
print_len("hello"); // &str is ?Sized — works because we take a reference

?Sized is primarily used for trait objects (dyn Trait is !Sized) and for [T] slices (which Are dynamically sized).

Supertraits define a dependency relationship between traits. A trait that requires another trait as A supertrait can only be implemented for types that also implement the supertrait:

trait Animal {
fn name(&self) -> &'static str;
}
trait Pet: Animal {
fn owner(&self) -> &'static str;
}
struct Dog {
owner_name: &'static str,
}
impl Animal for Dog {
fn name(&self) -> &'static str { "dog" }
}
impl Pet for Dog {
fn owner(&self) -> &'static str { self.owner_name }
}

The supertrait bound means any Pet can be used where an Animal is expected:

fn greet<T: Pet>(pet: &T) {
println!("{} (owned by {})", pet.name(), pet.owner());
}

Multiple supertraits are specified with +:

trait AdvancedDisplay: Display + Debug {
fn detailed_format(&self) -> String {
format!("display: {}, debug: {:?}", self, self)
}
}

Associated types define a type that is determined by the implementing type. They are the primary Mechanism for output types in trait definitions:

trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
count: u32,
}
impl Iterator for Counter {
type Item = u32; // associated type — determined by the implementation
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}

When should you use an associated type vs a generic parameter?

Use an associated type when:

  • The implementing type determines the associated type uniquely
  • There should be exactly one implementation per implementing type

Use a generic parameter when:

  • The implementing type can implement the trait for multiple types
  • The caller should choose the type parameter
// Associated type — one output type per implementor
trait Container {
type Element;
fn get(&self, index: usize) -> Option<&Self::Element>;
}
// Generic parameter — multiple implementations possible
trait Converter<T> {
fn convert(&self) -> T;
}
trait Graph {
type Node: Hash + Eq;
type Edge;
fn nodes(&self) -> Vec<Self::Node>;
fn edges_from(&self, node: &Self::Node) -> Vec<Self::Edge>;
}
trait BaudRate {
const DEFAULT: u32;
}
struct SerialPort;
impl BaudRate for SerialPort {
const DEFAULT: u32 = 115200;
}
fn configure<T: BaudRate>() -> u32 {
T::DEFAULT
}

Const generics allow you to use constant values as generic parameters. This is particularly useful For arrays and type-level programming:

struct Array<T, const N: usize> {
data: [T; N],
}
impl<T, const N: usize> Array<T, N> {
fn new() -> Self
where
T: Default,
{
Array {
data: std::array::from_fn(|_| T::default()),
}
}
fn len(&self) -> usize {
N
}
}
let arr: Array<i32, 10> = Array::new();
assert_eq!(arr.len(), 10);
struct Matrix<T, const ROWS: usize, const COLS: usize> {
data: [[T; COLS]; ROWS],
}
impl<T: Default + Copy, const ROWS: usize, const COLS: usize> Matrix<T, ROWS, COLS> {
fn identity() -> Self
where
T: std::ops::Add<Output = T>,
{
// ...
}
}
fn first<T, const N: usize>(arr: &[T; N]) -> Option<&T> {
if N > 0 {
Some(&arr[0])
} else {
None
}
}
## Intuition

Traits define shared behavior as interfaces, while generics enable code that works with multiple types. Trait bounds constrain generics to types that implement specific traits. Static dispatch (monomorphization) generates specialized code for each concrete type, while dynamic dispatch (trait objects) uses vtables for runtime polymorphism. The orphan rule prevents implementing foreign traits on foreign types, maintaining coherence across the ecosystem.

  • [[rust/03-structs-enums/structs-and-enums]] - Implementing traits for custom types
  • [[rust/04-error-handling/error-handling]] - Result with generic error types
  • [[rust/06-concurrency/concurrency]] - Send and Sync trait bounds
  • [[rust/07-cargo-ecosystem/cargo-and-ecosystem]] - Crate ecosystem and trait conventions