Intermediate Level

Generics

Chapter 11: Generics 🧬

Write flexible, reusable code with generics while maintaining Rust's type safety.

Generic Data Types

Generic Functions

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 number: {}", largest(&numbers));
    
    let chars = vec!['y', 'm', 'a', 'q'];
    println!("Largest char: {}", largest(&chars));
}

Generic Structs

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

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

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

fn main() {
    let integer_point = Point::new(5, 10);
    let float_point = Point::new(1.0, 4.0);
    
    println!("Distance: {}", float_point.distance_from_origin());
}

Generic Enums

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

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

fn main() {
    let some_number = Option::Some(5);
    let some_string = Option::Some("Hello");
    
    let success: Result<i32, String> = Result::Ok(42);
    let failure: Result<i32, String> = Result::Err(String::from("Error occurred"));
}

Generic Methods

Implementing Generic Methods

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

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

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

Traits as Parameters

Trait Bounds

trait Display {
    fn display(&self) -> String;
}

struct Person {
    name: String,
    age: u32,
}

impl Display for Person {
    fn display(&self) -> String {
        format!("{} is {} years old", self.name, self.age)
    }
}

fn print_display<T: Display>(item: T) {
    println!("{}", item.display());
}

fn main() {
    let person = Person {
        name: String::from("Alice"),
        age: 30,
    };
    
    print_display(person);
}

Multiple Trait Bounds

use std::fmt::Display;
use std::fmt::Debug;

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

// Alternative syntax using where clause
fn some_function_alt<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // Implementation
    0
}

Generic Collections

Working with Generic Vectors

fn process_items<T: Clone + std::fmt::Display>(items: Vec<T>) -> Vec<T> {
    let mut results = Vec::new();
    
    for item in items {
        println!("Processing: {}", item);
        results.push(item.clone());
    }
    
    results
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let processed_numbers = process_items(numbers);
    
    let strings = vec!["Hello", "World", "Rust"];
    let processed_strings = process_items(strings);
}

Performance Considerations

Monomorphization

fn main() {
    let integer_list = vec![1, 2, 3, 4, 5];
    let float_list = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    
    println!("Largest integer: {}", largest(&integer_list));
    println!("Largest float: {}", largest(&float_list));
}

This generates specialized code at compile time:

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
}

Practical Examples

Example 1: Generic Stack Implementation

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

impl<T> Stack<T> {
    fn new() -> Stack<T> {
        Stack { items: Vec::new() }
    }
    
    fn push(&mut self, item: T) {
        self.items.push(item);
    }
    
    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }
    
    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
    
    fn size(&self) -> usize {
        self.items.len()
    }
}

fn main() {
    let mut int_stack = Stack::new();
    int_stack.push(1);
    int_stack.push(2);
    int_stack.push(3);
    
    while let Some(item) = int_stack.pop() {
        println!("Popped: {}", item);
    }
    
    let mut string_stack = Stack::new();
    string_stack.push(String::from("Hello"));
    string_stack.push(String::from("World"));
    
    while let Some(item) = string_stack.pop() {
        println!("Popped: {}", item);
    }
}

Example 2: Generic Binary Search Tree

use std::cmp::Ordering;

struct Node<T> {
    value: T,
    left: Option<Box<Node<T>>>,
    right: Option<Box<Node<T>>>,
}

struct BST<T: PartialOrd> {
    root: Option<Box<Node<T>>>,
}

impl<T: PartialOrd> Node<T> {
    fn new(value: T) -> Node<T> {
        Node {
            value,
            left: None,
            right: None,
        }
    }
}

impl<T: PartialOrd> BST<T> {
    fn new() -> BST<T> {
        BST { root: None }
    }
    
    fn insert(&mut self, value: T) {
        match self.root {
            None => {
                self.root = Some(Box::new(Node::new(value)));
            }
            Some(ref mut node) => {
                Self::insert_recursive(node, value);
            }
        }
    }
    
    fn insert_recursive(node: &mut Box<Node<T>>, value: T) {
        match value.partial_cmp(&node.value).unwrap() {
            Ordering::Less => {
                match node.left {
                    None => {
                        node.left = Some(Box::new(Node::new(value)));
                    }
                    Some(ref mut left_node) => {
                        Self::insert_recursive(left_node, value);
                    }
                }
            }
            Ordering::Greater => {
                match node.right {
                    None => {
                        node.right = Some(Box::new(Node::new(value)));
                    }
                    Some(ref mut right_node) => {
                        Self::insert_recursive(right_node, value);
                    }
                }
            }
            Ordering::Equal => {
                // Value already exists, do nothing
            }
        }
    }
    
    fn search(&self, value: &T) -> bool {
        Self::search_recursive(&self.root, value)
    }
    
    fn search_recursive(node: &Option<Box<Node<T>>>, value: &T) -> bool {
        match node {
            None => false,
            Some(ref n) => {
                match value.partial_cmp(&n.value).unwrap() {
                    Ordering::Equal => true,
                    Ordering::Less => Self::search_recursive(&n.left, value),
                    Ordering::Greater => Self::search_recursive(&n.right, value),
                }
            }
        }
    }
}

fn main() {
    let mut bst = BST::new();
    bst.insert(5);
    bst.insert(3);
    bst.insert(7);
    bst.insert(1);
    bst.insert(9);
    
    println!("Contains 3: {}", bst.search(&3));
    println!("Contains 8: {}", bst.search(&8));
    
    let mut string_bst = BST::new();
    string_bst.insert(String::from("Rust"));
    string_bst.insert(String::from("Python"));
    string_bst.insert(String::from("JavaScript"));
    
    println!("Contains 'Rust': {}", string_bst.search(&String::from("Rust")));
    println!("Contains 'Java': {}", string_bst.search(&String::from("Java")));
}

Common Mistakes

āŒ Forgetting Trait Bounds

// This won't compile without trait bounds
fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];
    
    for &item in list {
        if item > largest { // Error: can't compare T values
            largest = item;
        }
    }
    
    largest
}

āœ… Adding Trait Bounds

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

Key Takeaways

  • āœ… Generics allow writing flexible, reusable code
  • āœ… Use trait bounds to specify what operations generic types support
  • āœ… Generic functions and structs are monomorphized at compile time
  • āœ… Multiple trait bounds can be specified using + or where clauses
  • āœ… Generics don't incur runtime performance penalties
  • āœ… Follow Rust naming conventions (T, U, V for generic types)

Ready for Chapter 12? → Traits

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source