RustGenerics

Generics in Rust

Generics let you write code that works with many different types without repeating yourself. Instead of writing a function that finds the largest i32, then another for f64, then another for char, you write one function parameterized over a type T — and the compiler figures out the rest.

The Problem Generics Solve

Imagine you want to find the largest element in a list. Without generics you'd need a separate function for every type you care about:

RUST
fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    for item in list {
        if item > largest { largest = item; }
    }
    largest
}

fn largest_f64(list: &[f64]) -> &f64 {
    let mut largest = &list[0];
    for item in list {
        if item > largest { largest = item; }
    }
    largest
}
// ... and so on for every type

The logic is identical — only the type changes. Generics let you write it once.

Generic Functions

Declare a type parameter between angle brackets after the function name. The bound PartialOrd tells the compiler that T supports the > operator:

RUST
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 numbers = vec![34, 50, 25, 100, 65];
    println!("Largest number: {}", largest(&numbers));

    let chars = vec!['y', 'm', 'a', 'q'];
    println!("Largest char: {}", largest(&chars));
}
Tip
The function signature reads: "for some type T that implements PartialOrd, take a slice of T and return a reference to T." The compiler verifies the bound at every call site.
Generic Structs

Structs can hold values of a generic type. Declare the type parameter after the struct name:

RUST
struct Pair<T> {
    first: T,
    second: T,
}

fn main() {
    let int_pair = Pair { first: 5, second: 10 };
    let str_pair = Pair { first: "hello", second: "world" };

    println!("{} {}", int_pair.first, int_pair.second);
    println!("{} {}", str_pair.first, str_pair.second);
}
Note
Both fields must be the same type T. If you need two independent types, use two parameters: `struct Pair<A, B>` with fields `first: A` and `second: B`.
Generic Enums

The standard library's most famous generic enums are Option and Result. Their definitions look exactly like user-defined generics:

RUST
// How Option<T> is defined in the standard library
enum Option<T> {
    Some(T),
    None,
}

// How Result<T, E> is defined
enum Result<T, E> {
    Ok(T),
    Err(E),
}

// Using them
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn read_number(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.trim().parse()
}

Result<T, E> uses two type parameters — one for the success value, one for the error. This is a common pattern whenever an operation can fail in structured ways.

impl Blocks for Generic Structs

When writing methods for a generic struct, repeat the type parameter on the impl keyword so the compiler knows it is a generic parameter and not a concrete type named T:

RUST
struct Pair<T> {
    first: T,
    second: T,
}

// impl<T> means "for any type T"
impl<T> Pair<T> {
    fn new(first: T, second: T) -> Self {
        Pair { first, second }
    }

    fn first(&self) -> &T {
        &self.first
    }
}

fn main() {
    let p = Pair::new(3, 7);
    println!("First: {}", p.first());
}
Conditional impl Blocks

You can add methods only when the type parameter satisfies extra traits. The method compare_display below is available only when T implements both Display and PartialOrd:

RUST
use std::fmt::Display;

impl<T: Display + PartialOrd> Pair<T> {
    fn compare_display(&self) {
        if self.first >= self.second {
            println!("First ({}) is larger or equal.", self.first);
        } else {
            println!("Second ({}) is larger.", self.second);
        }
    }
}

fn main() {
    let p = Pair::new(10, 20);
    p.compare_display(); // works because i32: Display + PartialOrd
}
Monomorphization — Zero Overhead

Rust generics have zero runtime cost. At compile time the compiler performs monomorphization: it replaces every generic instantiation with concrete code, as if you had written the specialized function by hand.

RUST
// You write this once:
fn identity<T>(x: T) -> T { x }

// The compiler generates (conceptually):
fn identity_i32(x: i32) -> i32 { x }
fn identity_str(x: &str) -> &str { x }
// ... one concrete copy per distinct type used in your program
Success
Because monomorphization happens at compile time, there is no boxing, no virtual dispatch, and no indirection — generic Rust code is as fast as hand-written specialized code.
Multiple Type Parameters

Functions and structs can have as many type parameters as needed. A zip function that combines two values of independent types:

RUST
fn zip<A, B>(a: A, b: B) -> (A, B) {
    (a, b)
}

fn main() {
    let pair = zip(42, "hello");
    println!("{} {}", pair.0, pair.1);

    let pair2 = zip(3.14, true);
    println!("{} {}", pair2.0, pair2.1);
}
Bounds on Struct Definitions vs impl Blocks

There is a subtle but important distinction: bounds placed on the struct definition are enforced whenever the struct is used, while bounds on impl blocks only apply to the methods in that block.

Rust's convention (and the compiler's preference) is to put bounds on impl blocks rather than struct definitions, unless the struct truly cannot be stored without the bound:

RUST
use std::fmt::Display;

// Bound on struct — every use of WrapperStrict<T> requires T: Display
struct WrapperStrict<T: Display> {
    value: T,
}

// Preferred: no bound on struct, bound only on the impl block that needs it
struct Wrapper<T> {
    value: T,
}

impl<T: Display> Wrapper<T> {
    fn show(&self) {
        println!("{}", self.value);
    }
}
// You can still create Wrapper<T> for types that don't implement Display
// as long as you don't call show() on them
Tip
Keeping struct definitions bound-free makes your types more flexible and avoids confusing errors when you only need the bound for certain methods.
T: Trait vs impl Trait Syntax

There are two ways to express "this parameter implements a trait." They look different but compile to the same thing for simple single-parameter cases:

RUST
use std::fmt::Display;

// Angle bracket / generic parameter form
fn print_generic<T: Display>(value: T) {
    println!("{}", value);
}

// impl Trait form (shorthand, introduced in Rust 2018)
fn print_impl(value: impl Display) {
    println!("{}", value);
}

// The generic form is required when T appears more than once
fn pair_generic<T: Display>(a: T, b: T) {
    println!("{} and {}", a, b);
}

// This does NOT enforce a == b type — each can be a different Display type
fn pair_wrong(a: impl Display, b: impl Display) {
    println!("{} and {}", a, b);
}
Note
Use `impl Trait` for simple single-use parameters. Use the angle-bracket form when you need to name the type (two parameters must be the same type) or when writing where clauses.
Where Clauses for Complex Bounds

When bounds grow long, move them to a where clause after the return type for readability:

RUST
use std::fmt::{Debug, Display};

// Hard to read with everything inline
fn verbose<T: Clone + Debug + Display, U: Clone + Debug>(t: T, u: U) -> String {
    format!("{} {:?}", t, u)
}

// Much cleaner with where clause
fn with_where<T, U>(t: T, u: U) -> String
where
    T: Clone + Debug + Display,
    U: Clone + Debug,
{
    format!("{} {:?}", t, u)
}

fn main() {
    println!("{}", with_where(42, vec![1, 2, 3]));
}
PhantomData

Sometimes a type logically "contains" or "is associated with" another type, but stores no actual value of that type. PhantomData<T> is a zero-sized marker that tells the compiler (and future readers) about this phantom relationship:

RUST
use std::marker::PhantomData;

// A typed ID — UserId and PostId are distinct types even though
// both wrap a plain u64 underneath.
struct Id<T> {
    value: u64,
    _marker: PhantomData<T>,
}

struct User;
struct Post;

type UserId = Id<User>;
type PostId = Id<Post>;

fn get_user(id: UserId) -> String {
    format!("user #{}", id.value)
}

fn main() {
    let uid: UserId = Id { value: 1, _marker: PhantomData };
    let pid: PostId = Id { value: 1, _marker: PhantomData };

    println!("{}", get_user(uid));
    // get_user(pid); // compile error: expected Id<User>, found Id<Post>
}

PhantomData is also used in unsafe code to communicate ownership and lifetime semantics to the compiler without storing an actual value of that type.

Summary

Concept

Syntax

Key Point

Generic function

fn f<T: Bound>(x: T)

Bound constrains what T can be

Generic struct

struct S<T> { field: T }

Type parameter declared after name

Generic enum

enum E<T> { A(T), B }

Option and Result are built-in examples

Generic impl

impl<T> S<T> { ... }

Repeat <T> on the impl keyword

Conditional impl

impl<T: Trait> S<T>

Methods only available for bounded types

Monomorphization

Compile-time specialization

Zero runtime overhead

Where clause

where T: A + B, U: C

Readable form of complex bounds

PhantomData

PhantomData<T>

Logical type relationship, zero size