Lesson 14

Generics

Write flexible, reusable code with generic types and trait bounds.

Generic Functions

Generics let you write code that works with many types without duplicating it. A generic function uses type parameters like <T> as placeholders. When you call the function, Rust figures out the concrete type automatically. This is called monomorphization -- there's zero runtime cost.

fn first<T>(list: &[T]) -> &T {
    &list[0]
}

Generic Structs

Structs can be generic too. Point<T> can hold any type for its coordinates -- integers, floats, whatever you need. You can even use multiple type parameters like Point<T, U> when the fields might have different types.

Trait Bounds

Often your generic code needs the type to support certain operations. Trait bounds restrict which types can be used: <T: Display + Clone> means "T must implement both Display and Clone." For complex bounds, use a where clause to keep things readable.

fn print_all<T>(items: &[T])
where
    T: std::fmt::Display,
{
    for item in items {
        println!("{}", item);
    }
}

Try it

use std::fmt::Display;

// A generic struct with one type parameter
#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

// Methods on a generic struct
impl<T: Display> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }

    fn describe(&self) {
        println!("  Point({}, {})", self.x, self.y);
    }
}

// Additional methods only for f64 Points
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

// A generic function with trait bounds
fn largest<T: PartialOrd + Display>(list: &[T]) -> &T {
    let mut biggest = &list[0];
    for item in &list[1..] {
        if item > biggest {
            biggest = item;
        }
    }
    biggest
}

// Generic function with a where clause
fn print_if_positive<T>(value: T)
where
    T: Display + PartialOrd + Default,
{
    if value > T::default() {
        println!("  {} is positive", value);
    } else {
        println!("  {} is not positive", value);
    }
}

fn main() {
    println!("--- Generic Point ---");
    let int_point = Point::new(5, 10);
    int_point.describe();

    let float_point = Point::new(3.0_f64, 4.0_f64);
    float_point.describe();
    println!("  Distance from origin: {:.2}", float_point.distance_from_origin());

    println!("\n--- Generic largest() ---");
    let numbers = vec![34, 50, 25, 100, 65];
    println!("  Largest number: {}", largest(&numbers));

    let words = vec!["apple", "mango", "banana", "cherry"];
    println!("  Largest word: {}", largest(&words));

    println!("\n--- Trait Bounds with where ---");
    print_if_positive(42);
    print_if_positive(-3);
    print_if_positive(0.0);
    print_if_positive(3.14);
}

Challenge

Make the Point struct work with mixed types by adding a second type parameter (e.g., Point<i32, f64>). Create a point with x as an integer and y as a float.

Previous: Traits

Next: Lifetimes