Traits and Generics
Trait Definition and Implementation
Section titled “Trait Definition and Implementation”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.
Defining a Trait
Section titled “Defining a Trait”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.
Implementing a Trait
Section titled “Implementing a Trait”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) }}Orphan Rule
Section titled “Orphan Rule”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
Section titled “Default Methods”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
Section titled “Trait Bounds”Trait bounds constrain generic types to those that implement specific traits.
Function Bounds
Section titled “Function Bounds”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());}Multiple Bounds
Section titled “Multiple Bounds”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,{ // ...}Bound with Lifetime
Section titled “Bound with Lifetime”fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a strwhere T: AsRef<str>,{ println!("announcement: {}", ann.as_ref()); if x.len() > y.len() { x } else { y }}Generics
Section titled “Generics”Generic Functions
Section titled “Generic Functions”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}Generic Structs
Section titled “Generic Structs”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 };Generic Enums
Section titled “Generic Enums”enum Option<T> { Some(T), None,}
enum Result<T, E> { Ok(T), Err(E),}Generic impl Blocks
Section titled “Generic impl Blocks”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); }}Generic Enums with Methods
Section titled “Generic Enums with Methods”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, } }}Monomorphization
Section titled “Monomorphization”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.
Comparing Static vs Dynamic Dispatch
Section titled “Comparing Static vs Dynamic Dispatch”// Static dispatch (monomorphized) — no vtable, inlinablefn process<T: Display>(item: T) { println!("{}", item);}
// Dynamic dispatch (vtable) — single copy, runtime lookupfn process_dyn(item: &dyn Display) { println!("{}", item);}| Property | Static Dispatch | Dynamic Dispatch |
|---|---|---|
| Overhead | None (after inlining) | Vtable lookup per call |
| Binary size | Larger (per type copy) | Smaller (single copy) |
| Inlining | Yes | No (indirect call) |
| Flexibility | Compile-time only | Runtime polymorphism |
| Cache behavior | Better (direct call) | Worse (indirect call) |
Trait Objects
Section titled “Trait Objects”dyn Trait
Section titled “dyn Trait”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();}Object Safety
Section titled “Object Safety”Not all traits can be used as trait objects. A trait is object safe if:
- It does not have any associated
constorfnitems with type parameters, generic methods, or methods that returnSelf(except&Self/&mut Self). - It does not have any associated
typethat usesSelfin non-trivial ways. - All methods have a receiver (
&self``&mut selfOrself). No associated functions.
// Object-safetrait 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.
Trait Object Casting
Section titled “Trait Object Casting”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; // upcastBlanket Implementations
Section titled “Blanket Implementations”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> { ... }Writing Your Own Blanket Implementations
Section titled “Writing Your Own Blanket Implementations”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 methodsimpl<T> ScalarOps for Twhere T: Copy + std::ops::Add<Output = T> + std::ops::Mul<Output = T>,{}Marker Traits
Section titled “Marker Traits”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
Section titled “Supertraits”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
Section titled “Associated Types”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 } }}Associated Types vs Generic Parameters
Section titled “Associated Types vs Generic Parameters”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 implementortrait Container { type Element; fn get(&self, index: usize) -> Option<&Self::Element>;}
// Generic parameter — multiple implementations possibletrait Converter<T> { fn convert(&self) -> T;}Associated Types with Bounds
Section titled “Associated Types with Bounds”trait Graph { type Node: Hash + Eq; type Edge;
fn nodes(&self) -> Vec<Self::Node>; fn edges_from(&self, node: &Self::Node) -> Vec<Self::Edge>;}Associated Constants
Section titled “Associated Constants”trait BaudRate { const DEFAULT: u32;}
struct SerialPort;impl BaudRate for SerialPort { const DEFAULT: u32 = 115200;}
fn configure<T: BaudRate>() -> u32 { T::DEFAULT}Const Generics
Section titled “Const Generics”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);Const Generics with Expressions
Section titled “Const Generics with Expressions”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>, { // ... }}Const Generic Bounds
Section titled “Const Generic Bounds”fn first<T, const N: usize>(arr: &[T; N]) -> Option<&T> { if N > 0 { Some(&arr[0]) } else { None }}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.
Cross-References
Section titled “Cross-References”- [[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