Skip to content
AgileLogics
5 min read

Generics

Prerequisites: HashMap & HashSet

Generics let one function or type work with many types. You have already used them: Vec<T>, Option<T> and Result<T, E> are all generic. The <T> is a type parameter — a placeholder that is filled in with a concrete type where the code is used.

Rust compiles generics by monomorphization: for each concrete type you use, the compiler generates a specialized copy of the code. Generic code therefore runs exactly as fast as code written by hand for each type; the cost is slightly longer compile times and larger binaries.

Syntax

fn first<T>(items: &[T]) -> Option<&T> {        // generic function
    items.first()
}

struct Pair<T> {                                  // generic struct
    left: T,
    right: T,
}

impl<T> Pair<T> {                                 // methods for every Pair<T>
    fn swap(self) -> Pair<T> {
        Pair { left: self.right, right: self.left }
    }
}

enum Shape<T> {                                   // generic enum
    Circle(T),
    Square(T),
}

A generic function can only do things that are valid for every possible T. To compare, add or print values you must say which abilities T needs with a trait bound, such as T: PartialOrd. Traits are the next lesson; for now, PartialOrd means "can be compared with < and >" and Copy means "can be copied like an integer".

Example

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

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }

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

// Methods that exist only for Point<f64>.
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

// Two type parameters: the fields can have different types.
#[derive(Debug)]
struct Labeled<K, V> {
    label: K,
    value: V,
}

fn main() {
    println!("{}", largest(&[34, 50, 25, 100, 65]));
    println!("{}", largest(&[1.5, 0.2, 7.25]));
    println!("{}", largest(&['y', 'm', 'a', 'q']));

    let int_point = Point::new(5, 10);
    let float_point = Point::new(3.0, 4.0);
    println!("{:?} has x = {}", int_point, int_point.x());
    println!("distance = {}", float_point.distance_from_origin());

    let item = Labeled { label: "answer", value: 42 };
    println!("{item:?} -> {}: {}", item.label, item.value);
}

Expected output

100
7.25
y
Point { x: 5, y: 10 } has x = 5
distance = 5
Labeled { label: "answer", value: 42 } -> answer: 42

int_point.distance_from_origin() would not compile: that method only exists for Point<f64>.

Const generics

Type parameters can also be constant values, most often array lengths:

fn sum_all<const N: usize>(values: [i32; N]) -> i32 {
    values.iter().sum()
}

fn main() {
    println!("{}", sum_all([1, 2, 3]));        // N = 3
    println!("{}", sum_all([10, 20, 30, 40])); // N = 4
}

This prints 6 and then 100.

Common mistakes

  • Forgetting the trait bound. Writing fn largest<T>(items: &[T]) -> T and then item > largest fails with error[E0369]: binary operation `>` cannot be applied to type `T` — the compiler tells you to add T: PartialOrd.
  • Forgetting <T> after impl. impl Point<T> { ... } makes Rust look for a concrete type called T: error[E0412]: cannot find type `T` in this scope. Write impl<T> Point<T>.
  • Mixing types in a single parameter: Point::new(5, 4.0) fails with error[E0308]: mismatched types because both fields share one T. Use two parameters (Point<T, U>) if that is really what you want.
  • Adding generics "just in case". A function that is only ever called with String is clearer taking &str or String directly.

Practice

Write a generic struct Stack<T> wrapping a Vec<T> with methods new, push, pop (returning Option<T>), and len. Push three string slices, pop one, and print the popped value and the remaining length.

Show solution
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 len(&self) -> usize {
        self.items.len()
    }
}

fn main() {
    let mut stack = Stack::new();
    stack.push("a");
    stack.push("b");
    stack.push("c");
    println!("popped {:?}", stack.pop());
    println!("{} left", stack.len());
}
popped Some("c")
2 left