[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