Generics
Generics let you write code that works for many types. Combined with trait bounds, you get C++-level zero-cost abstractions checked at compile time. Functions, structs, enums, impls — all generic.
Functions, structs, bounds, where
EXAMPLE
// 1) Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let nums = vec![10, 50, 25, 75, 33];
let chars = vec!['a', 'q', 'z', 'd'];
println!("{}", largest(&nums)); // 75
println!("{}", largest(&chars)); // z
}
// 2) Generic struct
struct Point<T> {
x: T,
y: T,
}
fn main() {
let int_pt = Point { x: 5, y: 10 };
let f_pt = Point { x: 1.0, y: 4.0 };
}
// 3) Multiple type parameters
struct Pair<A, B> {
first: A,
second: B,
}
let p: Pair<i32, String> = Pair { first: 42, second: "hi".to_string() };
// 4) Generic methods
impl<T> Point<T> {
fn x(&self) -> &T { &self.x }
fn y(&self) -> &T { &self.y }
}
// Method that only works for some types
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
// 5) Trait bounds
use std::fmt::Display;
fn print<T: Display>(value: T) {
println!("{}", value);
}
// Multiple bounds — +
fn print_compare<T: Display + PartialOrd>(a: T, b: T) {
if a > b { println!("{} > {}", a, b); }
else { println!("{} <= {}", a, b); }
}
// 6) where clause — cleaner for long bounds
fn process<T, U>(t: T, u: U) -> String
where
T: Display + Clone,
U: Clone + std::fmt::Debug,
{
format!("{} / {:?}", t.clone(), u.clone())
}
// 7) impl Trait — shorter syntax for single bound
fn print2(value: impl Display) {
println!("{}", value);
}
// As return type — opaque
fn make_closure() -> impl Fn(i32) -> i32 {
|x| x * 2
}
// 8) Generic enum — Option / Result are these!
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
enum Either<A, B> {
Left(A),
Right(B),
}
// 9) Generic implementations on traits
trait Greetable {
fn greet(&self);
}
impl<T: Display> Greetable for T {
fn greet(&self) {
println!("Hi, {}!", self);
}
}
// Now ANY Display type has .greet()
42.greet(); // Hi, 42!
"hello".greet(); // Hi, hello!
// 10) Default type parameters
trait Container<T = String> {
fn add(&mut self, item: T);
fn get(&self, idx: usize) -> Option<&T>;
}
// Use as Container or Container<i32>
// 11) Associated types — alternative to generics on traits
trait Iterator2 {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
// Each impl picks ONE Item type — cleaner than `trait Iterator<T>` where T can be anything.
// 12) Phantom types — track 'kinds' at compile time without storage
use std::marker::PhantomData;
struct Validated;
struct Unvalidated;
struct Email<State> {
address: String,
_state: PhantomData<State>,
}
impl Email<Unvalidated> {
fn new(s: String) -> Email<Unvalidated> {
Email { address: s, _state: PhantomData }
}
fn validate(self) -> Result<Email<Validated>, &'static str> {
if self.address.contains('@') {
Ok(Email { address: self.address, _state: PhantomData })
} else { Err("invalid") }
}
}
fn send(email: Email<Validated>) { /* only validated emails accepted */ }
// 13) Monomorphisation — zero-cost
// The compiler generates a SPECIALISED copy of generic code for each concrete type used.
// largest::<i32>(...) and largest::<char>(...) → two fully-specialised functions in assembly.
// Runtime: as fast as hand-written non-generic code.
// 14) const generics — generic over constants
struct Buffer<const N: usize> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self { Self { data: [0; N] } }
}
let b: Buffer<256> = Buffer::new(); // fixed-size buffer, compile-time size
// 15) Higher-rank trait bounds (HRTB)
fn apply_with_str<F>(f: F) -> usize
where
F: for<'a> Fn(&'a str) -> usize, // f works for ANY lifetime
{
f("hello")
}
// 16) Common bugs + patterns
// • Forgetting trait bound → 'method X doesn't exist' (add the bound)
// • Returning impl Trait from a function with branches → each branch must return same opaque type
// Fix: Box<dyn Trait>
// • Using generic where you should use dyn (runtime polymorphism vs compile-time)
// • Over-using PhantomData — usually associated types are clearer
// • Generic explosion in error messages → wrap trait bounds in named trait
// 17) Generic vs trait object (dyn)
// Generic : monomorphised, zero-cost, but binary grows + compile time grows
// Trait object: one impl, vtable lookup, runtime dispatch (~1ns), uniform binary
//
// Use generics for hot paths, libraries (zero-cost abstraction is the Rust promise).
// Use trait objects when storing heterogeneous types in one collection, or when
// compile times matter more than the 1-2 ns dynamic dispatch cost.
fn dyn_print(v: &[Box<dyn Display>]) {
for item in v {
println!("{}", item);
}
}
// 18) Common standard-library generic types
// Vec<T> — growable array
// HashMap<K, V>
// Option<T>, Result<T, E>
// Box<T>, Rc<T>, Arc<T>
// Iterator with associated Item type
// Cow<'a, T> — copy-on-write
// Cell<T>, RefCell<T> — interior mutability
// 19) Best practices
// • Start with concrete types; add generics when you actually need flexibility
// • Use impl Trait for simple cases; named generic for reuse + bounds
// • Keep bounds minimal — over-constraining limits callers
// • Use associated types in traits for cleaner APIs
// • Test generic code with at least 2 concrete instantiations
// • Read error messages carefully — Rust 1.70+ is much friendlier
Why it matters
Generics + trait bounds = zero-cost abstraction. Reach for them when the same code shape works for many types; use dyn Trait when you need a heterogeneous collection — the only place you give up monomorphisation.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
fn largest<T: PartialOrd + Copy>(xs: &[T]) -> T {
let mut best = xs[0];
for &x in xs { if x > best { best = x; } }
best
}
Try it Yourself »
Discussion
Loading…