You already know the problem: you want a function that works with integers AND strings AND your custom types. In dynamically typed languages, you just... don't specify types. In statically typed languages, you either duplicate code or use inheritance.

Rust gives you generics — write the code once, and the compiler generates specialized versions for each type you use. Zero runtime cost, full type safety.

Generic Functions

Here's a function that finds the largest element in a slice:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

The <T: PartialOrd> is a trait bound. It says: "T can be any type, as long as it implements PartialOrd (can be compared with >)."

Without the bound, the compiler wouldn't know if > works for T.

Generic Structs

You can genericize entire structs:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
}

The type parameter T gets filled in at use time. Point<i32>, Point<f64>, etc. Each use generates specialized code.

Multiple type parameters:

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let mixed = Point { x: 5, y: 4.0 };  // Point<i32, f64>
}

Generic Enums

You've already seen this. Option<T> is generic:

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Option<String> means "either a String or nothing." Result<i32, std::io::Error> means "either an i32 or an I/O error."

Generic Methods

You can implement methods on generic types:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// You can also constrain methods to specific types
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

The impl<T> says "this impl block is for Point of any type." The second impl is specifically for Point<f64>.

Trait Bounds

When you need to do something with a generic type, you need to constrain it:

fn largest<T: PartialOrd>(list: &[T]) -> &T { ... }

Multiple bounds use +:

fn notify<T: Summary + Display>(item: &T) { ... }

Or use where clauses for readability:

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // ...
}

The Performance Question

This is the key insight: generics are monomorphized.

fn largest<T: PartialOrd>(list: &[T]) -> &T { ... }

let nums = vec![1, 2, 3];
let result = largest(&nums);

When you call largest(&nums), the compiler generates:

fn largest_i32(list: &[i32]) -> &i32 { ... }

At compile time. Not runtime. The generic is a compile-time template that expands into specific functions. There's zero runtime overhead compared to writing the function twice.

This is called static dispatch — the function to call is known at compile time. (Contrast with trait objects, which use dynamic dispatch at runtime.)

Example: A Generic Stack

struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { items: Vec::new() }
    }
    
    fn push(&mut self, item: T) {
        self.items.push(item);
    }
    
    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }
    
    fn peek(&self) -> Option<&T> {
        self.items.last()
    }
}

fn main() {
    let mut int_stack = Stack::new();
    int_stack.push(1);
    int_stack.push(2);
    println!("{:?}", int_stack.pop());  // Some(2)
    
    let mut string_stack = Stack::new();
    string_stack.push("hello");
    string_stack.push("world");
    println!("{:?}", string_stack.pop());  // Some("world")
}

One struct, works with any type. No any type, no boxing, no runtime overhead.

The Big Picture

Generics are about abstraction without overhead:

| Approach | Type Safety | Performance | |----------|-------------|-------------| | void* in C | None | Fast | | interface{} in Go | Some | Boxing cost | | Generics in Rust | Full | Zero (monomorphization) |

Rust gives you the best of both worlds. You write abstract code, and the compiler makes it fast.


Next up: Chapter 12 — Building a Project. Put it all together.