iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…