[Avg. reading time: 8 minutes]

Generics

Generics allow you to write flexible, reusable code that works across multiple data types without duplication.

At compile time, Rust uses Monomorphization to replace generic placeholders with concrete types.

Monomorphization

  • Compiler generates type-specific versions of generic code
  • Happens at compile time, not runtime
  • Ensures no performance overhead

Why Generics?

  • Avoid code duplication
  • Improve readability and maintainability
  • Provide zero-cost abstraction
  • No runtime penalty

Key Concepts

  • T, U are generic type parameters
  • Trait bounds like Debug, Display, Add, Sub restrict what operations are allowed
  • Add<Output = T> ensures the result type matches input type
  • Multiple generic types allow flexibility across different inputs

Example


use std::fmt::Debug;
use std::fmt::Display;
use std::ops::*;

fn main() {
    //i32
    let n1: i32 = 10;
    let n2: i32 = 20;
    print_this(n1, n2);
    
    println!("Adding i32 ( {} + {} ) = {}", n1,n2, add_values(n1, n2));
    println!("Subtracting i32 ( {} from {} ) = {}", n1,n2,sub_values(n1, n2));
    println!("-----------------------------------");

    // F64
    let t1 = 23.45;
    let t2 = 34.56;
    print_this(t1, t2);

    println!("Adding f64 ( {} + {} ) = {}", t1,t2, add_values(t1, t2));
    println!("Subtracting f64 ( {} from {} ) = {}", t1,t2,sub_values(t1, t2));
    println!("-----------------------------------");

    // &str
    let r1 = "Rachel";
    let r2 = "Green";
    print_this(r1, r2);
    println!("----------------");

    // String Object
    let s1 = String::from("Rachel");
    let s2 = String::from("Green");
    print_this(s1, s2);
    println!("----------------");
    
    //printing diff datatypes
    
    print_another(t1,n1);

}


fn print_this<T: Debug + Display>(p1: T, p2: T) -> () {
    println!("Printing - {:?},{}", p1, p2)
}

fn add_values<T: Add<Output = T>>(p1: T, p2: T) -> T {
    // ://doc.rust-lang.org/std/ops/index.html
    p1 + p2
}

fn sub_values<T: Sub<Output = T>>(p1: T, p2: T) -> T {
    p1 - p2
}

fn print_another<T: Debug + Display, U:Debug + Display>(p1: T, p2: U) -> () {
    println!("Printing Diff Datatypes - {:?},{}", p1, p2)
}
  • T: Add, T: Sub - The generic datatype T should support + and -

Generics with Struct

// Generics with Struct

struct Sample<T, U, V> {
    a: T,
    b: U,
    c: V
}

fn main() {
    let var1 = Sample{ a: 43.10, b: 2, c:"Hello" };
    println!("A: {}", var1.a);
    println!("B: {}", var1.b);
    println!("C: {}", var1.c);

    println!("----------------");

    let var2 = Sample{ a: "Hello", b: "World", c:34.6 };
    println!("A: {}", var2.a);
    println!("B: {}", var2.b);
    println!("C: {}", var2.c);
}

Summary

  • Generics are resolved at compile time
  • No runtime cost compared to dynamic typing
  • Trait bounds are mandatory when operations require them
  • Overuse without bounds can lead to confusing compiler errors

#generics #monomorphizationVer 2.0.22

Last change: 2026-04-01