What Is Rust Generic?
Generics in Rust allow you to write code that can work with different data types without rewriting it again and again for each type.
Instead of fixing a type (like i32 or String) in your code, you write it generically, and Rust will automatically figure out the actual type when you use it.
It enables you to create functions, structs, enums, and traits that can operate on different types without sacrificing performance or safety.
We can use a generic parameter, which can represent any type. This approach avoids code duplication and increases reusability.
Benefits of Generics In Rust:
- Type Safety: Rust ensures that generics work correctly with the specified types at compile time.
- Reusability: It means you don’t need to duplicate logic for each data type.
- Performance: Generics are implemented using monomorphization, which replaces generics with actual types when compiling (zero runtime cost).
How To Use Generics in Functions
Generics in functions allow you to create one function that works with many types instead of writing a new one for each type.
Syntax:
fn function_name<T>(parameter: T) {
// Function body
}
In the example, T is a generic type parameter for any data type.
The part T: std::fmt::Debug means that T must be a type that can be printed with println!.
Example:
fn show_message<T: std::fmt::Display>(msg: T) {
println!("Message: {}", msg);
}
fn main() {
show_message(2025); // Works with an integer
show_message("Rust Rocks!"); // Works with a string
}
Output:
Message: 2025
Message: Rust Rocks!
- In this code, show_message() uses a generic T so it can accept any type that can be displayed. We didn’t need to write separate functions for i32 and &str.
How To Use Generics in Structs?
This means structs can also be made flexible with generics, just like functions. creating separate structs for integers, floats, or other types, we define a generic type placeholder T.
Code Example:
// A generic struct for holding a pair of values
struct Pair<T> {
first: T,
second: T,
}
fn main() {
let age_pair = Pair { first: 21, second: 30 }; // integers
let price_pair = Pair { first: 99.9, second: 149.5 }; // floats
println!("Age Pair: ({}, {})", age_pair.first, age_pair.second);
println!("Price Pair: ({}, {})", price_pair.first, price_pair.second);
}
Output:

- In this code, Pair<T> can hold any type as long as both values are of the same type.
- When we create age_pair, T becomes i32; for price_pair, T becomes f64.
How To Use Generics in Enums?
Enums can also use generics to hold different types in their variants.
When you create a variant, Rust fills in T with the actual type you provide. This allows a single enum definition to work for many data types.
Code Example:
// A generic enum to hold a success value or an error message
enum ResultValue<T> {
Success(T),
Error(String),
}
fn main() {
let success_number = ResultValue::Success(100); // integer
let success_text = ResultValue::Success("Done"); // string
let error_case = ResultValue::<i32>::Error(String::from("Something went wrong"));
match success_number {
ResultValue::Success(val) => println!("Success Number: {}", val),
ResultValue::Error(msg) => println!("Error: {}", msg),
}
match success_text {
ResultValue::Success(val) => println!("Success Text: {}", val),
ResultValue::Error(msg) => println!("Error: {}", msg),
}
match error_case {
ResultValue::Success(val) => println!("Success: {}", val),
ResultValue::Error(msg) => println!("Error Case: {}", msg),
}
}
Output:
Success Number: 100
Success Text: Done
Error Case: Something went wrong
- Here, ResultValue<T> is a generic enum. The Success variant can store any type (i32, &str, etc.), while Error stores a String.
Generic Constraints (Trait Bounds)
Generic Constraints (Trait Bounds) allow you to set the limit on the types that can be used with a generic.
Sometimes, we want to do certain operations like multiplication or comparison. Generic Constraints (Trait Bounds) allow us to do this.
Syntax:
fn function_name<T: TraitName>(parameter: T) {
// Function body
}
Example:
// Generic function to double a value that can be multiplied
fn double_value<T: std::ops::Mul<Output = T> + Copy>(val: T) -> T {
val * 2.into() // Multiply by 2
}
fn main() {
let int_val = 7;
let float_val = 4.5;
println!("Double of {} is {}", int_val, double_value(int_val));
println!("Double of {} is {}", float_val, double_value(float_val));
}
Output:
Double of 7 is 14
Double of 4.5 is 9
Generics with Multiple Type Parameters
Instead of using just one placeholder type (like T), we can use two or more (like T and U). This allows you to combine different types in a single structure.
Example:
// A struct that can store two different types
struct Info<T, U> {
name: T,
age: U,
}
fn main() {
// Here, T = &str (string slice), U = i32 (integer)
let person = Info {
name: "Alice",
age: 30,
};
// Here, T = String, U = f64
let company = Info {
name: String::from("TechCorp"),
age: 12.5, // company age in years
};
println!("Person: {} is {} years old", person.name, person.age);
println!("Company: {} has existed for {} years", company.name, company.age);
}
Output:
Person: Alice is 30 years old
Company: TechCorp has existed for 12.5 years
Generic Methods in Structs
We can create functions inside a struct that work with different types, even if the struct itself doesn’t store generic data.
Simple code example:
// A struct that provides math operations
struct MathOps;
impl MathOps {
// Generic method to multiply two values
fn multiply<T: std::ops::Mul<Output = T>>(a: T, b: T) -> T {
a * b
}
}
fn main() {
// Using the generic method with integers
let int_result = MathOps::multiply(4, 5);
// Using the generic method with floats
let float_result = MathOps::multiply(2.5, 3.0);
println!("Multiplication of integers: {}", int_result);
println!("Multiplication of floats: {}", float_result);
}
Output:
Multiplication of integers: 20
Multiplication of floats: 7.5
Monomorphization in Rust
Monomorphization is the process by which Rust replaces generic types with concrete types at compile time.
Generic functions or structs are converted into concrete versions for each type you use during compilation.
Think of it like making multiple models from a single template: each model is ready to handle a specific type.
Example Without Generics:
fn add_integers(a: i32, b: i32) -> i32 {
a + b
}
fn add_floats(a: f32, b: f32) -> f32 {
a + b
}
Example With Generics:
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
a + b
}
fn main() {
println!("Sum: {}", add(5, 10)); // i32
println!("Sum: {}", add(2.5, 3.5)); // f32
}
The generic function is monomorphized into two versions for i32 and f32 at compile time.
Exercise For Students:
Create a generic struct Storage<T> that can store a single item of any type. Then, create a generic method replace that allows updating the stored item with a new value. Finally, write a main function to test it with different types like i32, f64, and String.