Web Analytics

Generics - Generic Types and Functions

Advanced ~30 min read

Generics allow you to write code that works with multiple types while maintaining type safety. Instead of writing separate functions for each type, you can write one generic function that works with any type. Rust's generics have zero runtime cost thanks to monomorphization.

What Are Generics?

Generics are abstract stand-ins for concrete types. They let you write flexible, reusable code without sacrificing performance or type safety.

Zero-Cost Abstraction: Rust compiles generic code into specific code for each concrete type used (monomorphization). This means generics have no runtime overhead!

Generic Functions

Rust Generics
Output
Click Run to execute your code

Generic Type Parameters

Type parameters are specified in angle brackets <T>:

fn function_name(parameter: T) -> T {
    // T is a placeholder for any type
}

Generic Structs

Structs can use generic type parameters to hold values of any type:

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

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

Multiple Type Parameters

You can use multiple type parameters:

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

let mixed = Point { x: 5, y: 4.0 };  // x is i32, y is f64

Generic Enums

Rust's most common enums are generic:

enum Option {
    Some(T),
    None,
}

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

Generic Implementations

Output
Click Run to execute your code

Implementation Types

Type Syntax Use Case
Generic impl<T> Point<T> Methods for all types
Specific impl Point<f32> Methods for one type only
Constrained impl<T: Display> Point<T> Methods for types with trait

Combining Generics with Traits

Output
Click Run to execute your code
Pro Tip: Use trait bounds to constrain generic types. This ensures the type has the capabilities your code needs while keeping it generic.

Trait Bounds Syntax

Multiple ways to specify trait bounds:

// Inline syntax
fn notify(item: T) { }

// where clause (more readable for complex bounds)
fn notify(item: T)
where
    T: Summary + Display,
{ }

// impl Trait syntax (simpler for parameters)
fn notify(item: impl Summary + Display) { }

Monomorphization

Rust compiles generic code into specific code for each type used:

// You write this:
fn largest(list: &[T]) -> &T { ... }

let numbers = vec![1, 2, 3];
let chars = vec!['a', 'b', 'c'];
largest(&numbers);
largest(&chars);

// Compiler generates this:
fn largest_i32(list: &[i32]) -> &i32 { ... }
fn largest_char(list: &[char]) -> &char { ... }
Zero Runtime Cost: Because of monomorphization, generics have the same performance as hand-written code for each type!

Blanket Implementations

Implement a trait for any type that satisfies trait bounds:

// Standard library does this for ToString
impl ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

// Now any type with Display automatically has to_string()
let num = 42;
let s = num.to_string();  // Works!

Best Practices

  • Use descriptive type parameter names: T for type, E for error, K/V for key/value
  • Add trait bounds when needed: Constrain types to ensure required functionality
  • Use where clauses: For complex bounds, improve readability
  • Prefer generics over duplication: Write once, use with many types
  • Don't over-generalize: Only make things generic when needed
  • Combine with traits: Generics + traits = powerful abstractions

Common Mistakes

1. Forgetting trait bounds

// Wrong - T might not support comparison
fn largest(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {  // Error: can't compare T
            largest = item;
        }
    }
    largest
}

// Correct - Add PartialOrd bound
fn largest(list: &[T]) -> &T {
    // Now comparison works!
}

2. Mixing incompatible types in generic structs

// Wrong - x and y must be same type
struct Point {
    x: T,
    y: T,
}

let p = Point { x: 5, y: 4.0 };  // Error: i32 vs f64

// Correct - Use two type parameters
struct Point {
    x: T,
    y: U,
}

let p = Point { x: 5, y: 4.0 };  // OK!

3. Not specifying type parameters in impl blocks

// Wrong - Missing  after impl
impl Point {  // Error: can't find type T
    fn x(&self) -> &T {
        &self.x
    }
}

// Correct - Declare type parameter
impl Point {
    fn x(&self) -> &T {
        &self.x
    }
}

Exercise: Generics Practice

Task: Implement generic functions and a Container struct.

Output
Click Run to execute your code
Show Solution
use std::fmt::Display;

fn find_max(list: &[T]) -> Option<&T> {
    if list.is_empty() {
        return None;
    }
    
    let mut max = &list[0];
    for item in list {
        if item > max {
            max = item;
        }
    }
    Some(max)
}

struct Container {
    value: T,
}

impl Container {
    fn new(value: T) -> Self {
        Self { value }
    }
    
    fn get(&self) -> &T {
        &self.value
    }
    
    fn set(&mut self, value: T) {
        self.value = value;
    }
}

impl Container {
    fn print(&self) {
        println!("Container holds: {}", self.value);
    }
}

fn swap(a: &mut T, b: &mut T) {
    std::mem::swap(a, b);
}

Summary

  • Generics enable code reuse across multiple types
  • Type parameters are specified in angle brackets <T>
  • Work with functions, structs, enums, and implementations
  • Trait bounds constrain generic types
  • Multiple type parameters with <T, U, V>
  • where clauses for complex bounds
  • Monomorphization creates specific code at compile time
  • Zero runtime cost - same performance as non-generic code
  • Blanket implementations apply traits to many types
  • Combine with traits for powerful abstractions

What's Next?

Now that you understand generics and traits, you're ready to explore more advanced topics like Closures (anonymous functions) and Iterators (lazy evaluation). These features work together with generics and traits to enable functional programming patterns in Rust.