Guide contents

Lesson 31 of 94

Generics

Generics let you write code once for many types. Rust generics work through monomorphization: the compiler generates a separate, specialized copy of generic code for each concrete type used. Vec<i32> and Vec<String> are different types with different machine code, and generic code is as fast as hand-written code for each type.

That explains what Rust generics can do: work directly with primitives, call T::default() or T::new(), specialize methods for particular types, and use constant values as parameters.

Concept Rust
Generic function fn first<T>(list: &[T]) -> &T
Bounded type parameter <T: Ord>
Generic struct struct Wrapper<T> { value: T }
Generic collection Vec<i32> (unboxed, contiguous)
Creating a T T::default() or T::new() via a trait bound
How generics compile A specialized copy per type (monomorphization)

1. The problem: duplicated code

Two functions with identical logic, differing only in type:

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

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

fn main() {
    println!("{}", largest_i32(&[34, 50, 25, 100, 65]));
    println!("{}", largest_char(&['y', 'm', 'a', 'q']));
}
Output
100
y

2. A generic function needs to say what it requires

Replacing the types with a type parameter T is not enough: the compiler refuses to compile item > largest because not every type can be compared.

Rust✗ does not compile · E0369
fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    println!("{}", largest(&[1, 2, 3]));
}
Output
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:4:17
  |
4 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

This is a key difference from C++ templates: Rust checks a generic function once, against its declared requirements, not separately for each type it is used with. Errors are reported where the generic code is written, and they're readable.

3. Trait bounds: T: PartialOrd

A trait bound says what a type parameter must be able to do. T: PartialOrd means "any type that supports < and >" (the trait from Deriving Standard Traits).

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() {
    println!("{}", largest(&[34, 50, 25, 100, 65]));
    println!("{}", largest(&['y', 'm', 'a', 'q']));
    println!("{}", largest(&[1.5, -2.0, 0.25]));
    println!("{}", largest(&["pear", "apple", "zucchini"]));
}
Output
100
y
1.5
zucchini

4. Returning T by value needs Copy

To return the largest element by value rather than as a reference, the function must copy it out of the slice, which is only allowed if T: Copy (Move, Copy & Clone). Several bounds are combined with +.

Rust
fn largest<T: PartialOrd + Copy>(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(&numbers));
    let chars = vec!['y', 'm', 'a', 'q'];
    println!("{}", largest(&chars));
}
Output
100
y

Every piece of this signature has now been covered: generics, the trait bound PartialOrd, the Copy trait, the slice &[T], and the for &item pattern.

5. Without Copy, moving out of the slice fails

With Strings, which are not Copy, the by-value version doesn't compile, while the reference version from example 3 works fine.

Rust✗ does not compile · E0277
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let words = vec![String::from("pear"), String::from("apple")];
    println!("{}", largest(&words));
}
Output
error[E0277]: the trait bound `String: Copy` is not satisfied
  --> src/main.rs:13:28
   |
13 |     println!("{}", largest(&words));
   |                    ------- ^^^^^^ the trait `Copy` is not implemented for `String`
   |                    |
   |                    required by a bound introduced by this call
   |
note: required by a bound in `largest`
  --> src/main.rs:1:28
   |
 1 | fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
   |                            ^^^^ required by this bound in `largest`

Alternatives: return &T (example 3), require T: Clone and call .clone(), or take the vector by value and move the result out.

6. Generic structs

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

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
    let labels = Point { x: "left", y: "top" };
    println!("{:?} {:?} {:?}", integer, float, labels);
}
Output
Point { x: 5, y: 10 } Point { x: 1.0, y: 4.0 } Point { x: "left", y: "top" }

7. One type parameter means one type

Point<T> has both fields of the same T, so mixing an integer and a float is an error.

Rust✗ does not compile · E0308
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let p = Point { x: 5, y: 4.0 };
}
Output
error[E0308]: mismatched types
 --> src/main.rs:7:30
  |
7 |     let p = Point { x: 5, y: 4.0 };
  |                              ^^^ expected integer, found floating-point number

8. Several type parameters

Rust
#[derive(Debug)]
struct Pair<K, V> {
    key: K,
    value: V,
}

fn main() {
    let setting = Pair { key: "timeout_ms", value: 5_000 };
    let flag = Pair { key: String::from("beta"), value: true };
    let nested = Pair { key: 1, value: Pair { key: 'a', value: 2.5 } };
    println!("{:?}\n{:?}\n{:?}", setting, flag, nested);
}
Output
Pair { key: "timeout_ms", value: 5000 }
Pair { key: "beta", value: true }
Pair { key: 1, value: Pair { key: 'a', value: 2.5 } }

9. Methods on generic structs

impl<T> Point<T> declares T for the impl block, so the methods work for every Point<T>.

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

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

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

    fn swap(self) -> Point<T> {
        Point { x: self.y, y: self.x }
    }
}

fn main() {
    let p = Point::new("east", "north");
    println!("x = {}", p.x());
    let q = p.swap();
    println!("after swap: ({}, {})", q.x, q.y);
}
Output
x = east
after swap: (north, east)

10. Methods only for specific types

An impl block for a concrete type adds methods that exist only for that type.

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

impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

impl Point<&str> {
    fn describe(&self) -> String {
        format!("{} and {}", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("{}", p.distance_from_origin());
    let d = Point { x: "up", y: "down" };
    println!("{}", d.describe());
    // Point { x: 3, y: 4 }.distance_from_origin(); // no such method on Point<i32>
}
Output
5
up and down

11. Methods with their own type parameters

A method can introduce new type parameters in addition to the struct's.

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

impl<X, Y> Point<X, Y> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X, Y2> {
        Point { x: self.x, y: other.y }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };
    println!("{:?}", p1.mixup(p2));
}
Output
Point { x: 5, y: 'c' }

12. Generic enums

Option<T> and Result<T, E> are generic enums. You can define your own the same way.

Rust
#[derive(Debug)]
enum Either<L, R> {
    Left(L),
    Right(R),
}

fn parse_id(input: &str) -> Either<u64, String> {
    match input.parse::<u64>() {
        Ok(n) => Either::Left(n),
        Err(_) => Either::Right(input.to_lowercase()),
    }
}

fn main() {
    for input in ["1042", "Ferris", "7"] {
        match parse_id(input) {
            Either::Left(id) => println!("numeric id {id}"),
            Either::Right(username) => println!("username {username}"),
        }
    }
}
Output
numeric id 1042
username ferris
numeric id 7

13. A generic recursive enum: a binary tree

Rust
#[derive(Debug)]
enum Tree<T> {
    Leaf,
    Node(Box<Tree<T>>, T, Box<Tree<T>>),
}

impl<T: PartialOrd> Tree<T> {
    fn insert(self, value: T) -> Tree<T> {
        match self {
            Tree::Leaf => Tree::Node(Box::new(Tree::Leaf), value, Box::new(Tree::Leaf)),
            Tree::Node(left, v, right) => {
                if value < v {
                    Tree::Node(Box::new(left.insert(value)), v, right)
                } else {
                    Tree::Node(left, v, Box::new(right.insert(value)))
                }
            }
        }
    }

    fn in_order<'a>(&'a self, out: &mut Vec<&'a T>) {
        if let Tree::Node(left, v, right) = self {
            left.in_order(out);
            out.push(v);
            right.in_order(out);
        }
    }
}

fn main() {
    let mut tree = Tree::Leaf;
    for n in [50, 30, 70, 20, 40, 60] {
        tree = tree.insert(n);
    }
    let mut sorted = Vec::new();
    tree.in_order(&mut sorted);
    println!("{:?}", sorted);

    let mut words = Tree::Leaf;
    for w in ["pear", "apple", "fig"] {
        words = words.insert(w);
    }
    let mut out = Vec::new();
    words.in_order(&mut out);
    println!("{:?}", out);
}
Output
[20, 30, 40, 50, 60, 70]
["apple", "fig", "pear"]

impl<T: PartialOrd> Tree<T> puts the bound on the whole impl block, so these methods exist only for trees of comparable values.

14. The turbofish: naming the type explicitly

When the compiler can't infer a type parameter, you give it with ::<Type>, nicknamed the turbofish. It is needed most with parse, collect, and functions whose type parameter only appears in the return type.

Rust
fn zero<T: Default>() -> T {
    T::default()
}

fn main() {
    let n = "42".parse::<u8>().unwrap();
    let words = "a b c".split(' ').collect::<Vec<_>>();
    let z = zero::<f64>();
    let s = zero::<String>();
    println!("{n} {:?} {z} {:?}", words, s);
}
Output
42 ["a", "b", "c"] 0 ""

Vec<_> lets the compiler fill in the element type while you pick the collection.

15. When type inference needs help

Rust✗ does not compile · E0282
fn main() {
    let items = Vec::new();
    println!("{}", items.len());
}
Output
error[E0282]: type annotations needed for `Vec<_>`
 --> src/main.rs:2:9
  |
2 |     let items = Vec::new();
  |         ^^^^^   ---------- type must be known at this point
  |
help: consider giving `items` an explicit type, where the type for type parameter `T` is specified
  |
2 |     let items: Vec<T> = Vec::new();
  |              ++++++++

Nothing tells the compiler what the vector holds. An annotation (let items: Vec<String> = Vec::new();), a turbofish (Vec::<String>::new()), or a later push fixes it.

16. Creating values of type T

Because generic code is specialized for each type, it can call associated functions on T, such as T::default().

Rust
#[derive(Debug, Default)]
struct Stats {
    hits: u64,
    misses: u64,
}

fn fresh_pair<T: Default>() -> (T, T) {
    (T::default(), T::default())
}

fn main() {
    let (a, b): (Stats, Stats) = fresh_pair();
    let (x, y) = fresh_pair::<Vec<u8>>();
    println!("{:?} {:?} {:?} {:?}", a, b, x, y);
}
Output
Stats { hits: 0, misses: 0 } Stats { hits: 0, misses: 0 } [] []

17. Monomorphization: one copy per type

std::any::type_name::<T>() reveals which concrete type each copy of a generic function was generated for.

Rust
fn describe<T>(_value: &T) -> &'static str {
    std::any::type_name::<T>()
}

fn main() {
    println!("{}", describe(&42_u8));
    println!("{}", describe(&3.5));
    println!("{}", describe(&"text"));
    println!("{}", describe(&vec![1, 2, 3]));
    println!("{}", describe(&Some('x')));
}
Output
u8
f64
&str
alloc::vec::Vec<i32>
core::option::Option<char>

The compiler generated five versions of describe, one for each type. The cost of monomorphization is binary size and compile time, not runtime speed.

18. Generic types are not boxed

A Vec<i32> holds the numbers themselves, side by side, and each specialization has its own exact size.

Rust
struct Wrapper<T> {
    value: T,
    flag: bool,
}

fn main() {
    println!("Wrapper<u8>:   {} bytes", std::mem::size_of::<Wrapper<u8>>());
    println!("Wrapper<u64>:  {} bytes", std::mem::size_of::<Wrapper<u64>>());
    println!("Wrapper<[u8; 100]>: {} bytes", std::mem::size_of::<Wrapper<[u8; 100]>>());
    let v: Vec<i32> = (1..=1000).collect();
    println!("1000 i32s use {} bytes of element storage", v.len() * std::mem::size_of::<i32>());
    let w = Wrapper { value: 1_u8, flag: true };
    println!("{} {}", w.value, w.flag);
}
Output
Wrapper<u8>:   2 bytes
Wrapper<u64>:  16 bytes
Wrapper<[u8; 100]>: 101 bytes
1000 i32s use 4000 bytes of element storage
1 true

19. Generic functions with &mut T

A generic function with no bounds can still move, swap, and store values. It just can't do anything type-specific with them.

Rust
fn rotate_left<T>(a: &mut T, b: &mut T, c: &mut T) {
    std::mem::swap(a, b); // a=b, b=a
    std::mem::swap(b, c); // b=c, c=a
}

fn main() {
    let (mut x, mut y, mut z) = (1, 2, 3);
    rotate_left(&mut x, &mut y, &mut z);
    println!("{x} {y} {z}");

    let (mut p, mut q, mut r) = (String::from("red"), String::from("green"), String::from("blue"));
    rotate_left(&mut p, &mut q, &mut r);
    println!("{p} {q} {r}");
}
Output
2 3 1
green blue red

20. Const generics: numbers as type parameters

A type parameter can be a constant value, such as an array length. [T; N] works for arrays of any size, which was impossible with the fixed-size arrays in Tuples & Arrays.

Rust
fn sum<const N: usize>(values: [i32; N]) -> i32 {
    let mut total = 0;
    for v in values {
        total += v;
    }
    total
}

fn first_and_last<T: Copy, const N: usize>(values: [T; N]) -> Option<(T, T)> {
    if N == 0 { None } else { Some((values[0], values[N - 1])) }
}

fn main() {
    println!("{}", sum([1, 2, 3]));
    println!("{}", sum([10, 20, 30, 40, 50]));
    println!("{:?}", first_and_last(['a', 'b', 'c']));
    println!("{:?}", first_and_last::<u8, 0>([]));
}
Output
6
150
Some(('a', 'c'))
None

21. Const generics in structs: a fixed-size matrix

The dimensions become part of the type, so multiplying matrices with incompatible sizes is a compile error, not a runtime exception.

Rust
#[derive(Debug, Clone, Copy)]
struct Matrix<const R: usize, const C: usize> {
    data: [[i64; C]; R],
}

impl<const R: usize, const C: usize> Matrix<R, C> {
    fn multiply<const K: usize>(&self, other: &Matrix<C, K>) -> Matrix<R, K> {
        let mut data = [[0; K]; R];
        for i in 0..R {
            for j in 0..K {
                for k in 0..C {
                    data[i][j] += self.data[i][k] * other.data[k][j];
                }
            }
        }
        Matrix { data }
    }
}

fn main() {
    let a = Matrix { data: [[1, 2, 3], [4, 5, 6]] };        // 2x3
    let b = Matrix { data: [[7, 8], [9, 10], [11, 12]] };    // 3x2
    let c = a.multiply(&b);                                   // 2x2
    println!("{:?}", c.data);
    // b.multiply(&b); // does not compile: 3x2 times 3x2
}
Output
[[58, 64], [139, 154]]

22. A generic stack

Rust
#[derive(Debug)]
struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Self { 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 len(&self) -> usize {
        self.items.len()
    }

    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

fn main() {
    let mut history: Stack<String> = Stack::new();
    history.push(String::from("/home"));
    history.push(String::from("/products"));
    history.push(String::from("/products/42"));
    println!("current: {:?}", history.peek());
    println!("back from: {:?}", history.pop());
    println!("now at: {:?}, {} in history", history.peek(), history.len());

    let mut numbers = Stack::new();
    numbers.push(1);
    numbers.push(2);
    while let Some(n) = numbers.pop() {
        print!("{n} ");
    }
    println!("empty: {}", numbers.is_empty());
}
Output
current: Some("/products/42")
back from: Some("/products/42")
now at: Some("/products"), 2 in history
2 1 empty: true

23. Typed IDs with PhantomData

A UserId and an OrderId are both numbers, and mixing them up is a classic bug. A generic Id<T> makes each entity's ID a distinct type. The struct doesn't store a T, so PhantomData<T> (a zero-sized marker) tells the compiler that the type parameter is used on purpose.

Rust
use std::marker::PhantomData;

#[derive(Debug)]
struct Id<T> {
    value: u64,
    _entity: PhantomData<T>,
}

impl<T> Id<T> {
    fn new(value: u64) -> Self {
        Self { value, _entity: PhantomData }
    }
}

struct User;
struct Order;

fn cancel_order(user: &Id<User>, order: &Id<Order>) {
    println!("user {} cancels order {}", user.value, order.value);
}

fn main() {
    let user = Id::<User>::new(7);
    let order = Id::<Order>::new(9001);
    cancel_order(&user, &order);
    // cancel_order(&order, &user); // does not compile: expected Id<User>, found Id<Order>
    println!("size of Id<User>: {} bytes", std::mem::size_of::<Id<User>>());
}
Output
user 7 cancels order 9001
size of Id<User>: 8 bytes

The marker costs nothing at runtime: Id<User> is exactly 8 bytes.

24. Mixing up typed IDs is a compile error

Rust✗ does not compile · E0308
use std::marker::PhantomData;

struct Id<T> {
    value: u64,
    _entity: PhantomData<T>,
}

struct User;
struct Order;

fn load_user(id: Id<User>) -> u64 {
    id.value
}

fn main() {
    let order_id: Id<Order> = Id { value: 9001, _entity: PhantomData };
    load_user(order_id);
}
Output
error[E0308]: mismatched types
  --> src/main.rs:17:15
   |
17 |     load_user(order_id);
   |     --------- ^^^^^^^^ expected `Id<User>`, found `Id<Order>`
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected struct `Id<User>`
              found struct `Id<Order>`
note: function defined here
  --> src/main.rs:11:4
   |
11 | fn load_user(id: Id<User>) -> u64 {
   |    ^^^^^^^^^ ------------

25. Realistic example: a generic API response

Rust
#[derive(Debug)]
enum ApiResponse<T> {
    Ok { data: T, request_id: String },
    NotFound { resource: String },
    Error { status: u16, message: String },
}

impl<T> ApiResponse<T> {
    fn data(self) -> Option<T> {
        match self {
            ApiResponse::Ok { data, .. } => Some(data),
            _ => None,
        }
    }

    fn status(&self) -> u16 {
        match self {
            ApiResponse::Ok { .. } => 200,
            ApiResponse::NotFound { .. } => 404,
            ApiResponse::Error { status, .. } => *status,
        }
    }
}

#[derive(Debug)]
struct User {
    id: u32,
    name: String,
}

fn get_user(id: u32) -> ApiResponse<User> {
    match id {
        1 => ApiResponse::Ok { data: User { id: 1, name: String::from("Ada") }, request_id: String::from("req-1") },
        2 => ApiResponse::Error { status: 503, message: String::from("database unavailable") },
        _ => ApiResponse::NotFound { resource: format!("user/{id}") },
    }
}

fn get_count() -> ApiResponse<u64> {
    ApiResponse::Ok { data: 1_234, request_id: String::from("req-2") }
}

fn main() {
    for id in [1, 2, 3] {
        let r = get_user(id);
        println!("{} {:?}", r.status(), r);
    }
    println!("count = {:?}", get_count().data());
}
Output
200 Ok { data: User { id: 1, name: "Ada" }, request_id: "req-1" }
503 Error { status: 503, message: "database unavailable" }
404 NotFound { resource: "user/3" }
count = Some(1234)

26. Realistic example: a paginated result

Rust
#[derive(Debug)]
struct Page<T> {
    items: Vec<T>,
    page: usize,
    total_items: usize,
    page_size: usize,
}

impl<T> Page<T> {
    fn total_pages(&self) -> usize {
        self.total_items.div_ceil(self.page_size)
    }

    fn has_next(&self) -> bool {
        self.page + 1 < self.total_pages()
    }
}

fn paginate<T: Clone>(all: &[T], page: usize, page_size: usize) -> Page<T> {
    let start = (page * page_size).min(all.len());
    let end = (start + page_size).min(all.len());
    Page { items: all[start..end].to_vec(), page, total_items: all.len(), page_size }
}

fn main() {
    let products = ["mouse", "keyboard", "monitor", "webcam", "headset"];
    let mut p = 0;
    loop {
        let page = paginate(&products, p, 2);
        println!("page {}/{}: {:?}", page.page + 1, page.total_pages(), page.items);
        if !page.has_next() {
            break;
        }
        p += 1;
    }
    let ids: Vec<u32> = (1..=3).collect();
    println!("{:?}", paginate(&ids, 0, 10));
}
Output
page 1/3: ["mouse", "keyboard"]
page 2/3: ["monitor", "webcam"]
page 3/3: ["headset"]
Page { items: [1, 2, 3], page: 0, total_items: 3, page_size: 10 }

paginate requires T: Clone because to_vec() copies the page's items into the new Vec.

27. Realistic example: a small generic cache

A least-recently-inserted cache that works for any key and value types. The bounds say exactly what it needs: keys must be comparable with ==, and values must be cloneable so get can return a copy.

Rust
struct Cache<K, V> {
    entries: Vec<(K, V)>,
    capacity: usize,
    hits: u32,
    misses: u32,
}

impl<K: PartialEq, V: Clone> Cache<K, V> {
    fn new(capacity: usize) -> Self {
        Self { entries: Vec::new(), capacity, hits: 0, misses: 0 }
    }

    fn get(&mut self, key: &K) -> Option<V> {
        match self.entries.iter().find(|(k, _)| k == key) {
            Some((_, v)) => {
                self.hits += 1;
                Some(v.clone())
            }
            None => {
                self.misses += 1;
                None
            }
        }
    }

    fn put(&mut self, key: K, value: V) {
        self.entries.retain(|(k, _)| *k != key);
        if self.entries.len() == self.capacity {
            self.entries.remove(0); // evict the oldest
        }
        self.entries.push((key, value));
    }
}

fn expensive_lookup(id: u32) -> String {
    format!("profile-{id}")
}

fn main() {
    let mut cache: Cache<u32, String> = Cache::new(2);
    for id in [1, 2, 1, 3, 2, 1] {
        let profile = match cache.get(&id) {
            Some(p) => p,
            None => {
                let p = expensive_lookup(id);
                cache.put(id, p.clone());
                p
            }
        };
        println!("{id}: {profile}");
    }
    println!("hits={} misses={}", cache.hits, cache.misses);
}
Output
1: profile-1
2: profile-2
1: profile-1
3: profile-3
2: profile-2
1: profile-1
hits=2 misses=4

28. Realistic example: a generic min-max finder

One function works for numbers, strings, dates, or any ordered type, and returns references so nothing is copied.

Rust
fn min_max<T: PartialOrd>(items: &[T]) -> Option<(&T, &T)> {
    let first = items.first()?;
    let mut min = first;
    let mut max = first;
    for item in &items[1..] {
        if item < min {
            min = item;
        }
        if item > max {
            max = item;
        }
    }
    Some((min, max))
}

#[derive(Debug, PartialEq, PartialOrd)]
struct Date {
    year: u16,
    month: u8,
    day: u8,
}

fn main() {
    println!("{:?}", min_max(&[3.5, -1.0, 8.25, 0.0]));
    println!("{:?}", min_max(&["kiwi", "apple", "mango"]));
    let dates = [
        Date { year: 2026, month: 3, day: 14 },
        Date { year: 2025, month: 12, day: 31 },
        Date { year: 2026, month: 9, day: 27 },
    ];
    if let Some((earliest, latest)) = min_max(&dates) {
        println!("{:?} .. {:?}", earliest, latest);
    }
    let empty: [u8; 0] = [];
    println!("{:?}", min_max(&empty));
}
Output
Some((-1.0, 8.25))
Some(("apple", "mango"))
Date { year: 2025, month: 12, day: 31 } .. Date { year: 2026, month: 9, day: 27 }
None

Date derives PartialOrd, which compares year, then month, then day, so it works with min_max without any extra code.

Summary

  • fn name<T>(...), struct Name<T>, enum Name<T>, and impl<T> Name<T> declare type parameters.
  • Bounds (T: PartialOrd + Copy) state what a type must support. Generic code is checked once against its bounds.
  • impl Point<f64> adds methods only for one concrete type. Methods can have their own type parameters.
  • The turbofish ::<T> names a type when inference can't.
  • Rust monomorphizes: each concrete type gets its own compiled copy. There is no boxing and no erasure, so T::default() works and Vec<i32> stores plain integers.
  • Const generics (const N: usize) put numbers such as array lengths into types.
  • PhantomData<T> marks a type parameter that is used only at the type level, as in typed IDs.

Next: Trait Bounds & where Clauses.