Rust Generics

Rust Generics: A Comprehensive Guide with Examples

Generics in Rust allow you to write flexible, reusable, and type-safe code. They enable you to create functions, structs, enums, and traits that can operate on different types without sacrificing performance or safety. Generics are a core feature of Rust, ensuring type abstraction while maintaining strict compile-time checks.

What Are Generics?

Generics allow defining a placeholder for data types. Instead of specifying a concrete type, you use a generic parameter, which can represent any type. This approach avoids code duplication and increases reusability.

Key Benefits of Generics:

  1. Type Safety: Rust ensures that generics work correctly with the specified types at compile time.
  2. Reusability: Avoid duplicating code for different types.
  3. Performance: Generics are implemented using monomorphization, which replaces generic types with specific ones at compile time, resulting in zero runtime overhead.

Using Generics in Functions

Generics in functions allow you to write a single function that works with multiple data types.

Syntax:

fn function_name<T>(parameter: T) {
// Function body
}

Here, T is a generic type parameter.

Example:

fn print_value<T: std::fmt::Debug>(value: T) {
println!("{:?}", value);
}

fn main() {
print_value(42); // Integer
print_value("Hello"); // String
}

Output:

42
"Hello"

Using Generics in Structs

Generics in structs allow you to define data structures that can hold multiple types.

Example:

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

fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.5, y: 2.5 };

println!("Integer Point: ({}, {})", integer_point.x, integer_point.y);
println!("Float Point: ({}, {})", float_point.x, float_point.y);
}

Output:

Integer Point: (5, 10)
Float Point: (1.5, 2.5)

Using Generics in Enums

Enums can also use generics to handle different types.

Example:

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

fn main() {
let some_number = Option::Some(42);
let no_number: Option<i32> = Option::None;

match some_number {
Option::Some(value) => println!("Value: {}", value),
Option::None => println!("No value"),
}
}

Output:

Value: 42

Generic Constraints (Trait Bounds)

Sometimes, you want to restrict the types that can be used with a generic. You can achieve this using trait bounds.

Syntax:

fn function_name<T: TraitName>(parameter: T) {
// Function body
}

Example:

fn calculate_square<T: std::ops::Mul<Output = T> + Copy>(value: T) -> T {
value * value
}

fn main() {
println!("Square of 5: {}", calculate_square(5));
println!("Square of 3.5: {}", calculate_square(3.5));
}

Output:

Square of 5: 25
Square of 3.5: 12.25

Generics with Multiple Type Parameters

You can use multiple generic type parameters to make your code more flexible.

Example:

struct Pair<T, U> {
first: T,
second: U,
}

fn main() {
let pair = Pair {
first: "Hello",
second: 42,
};

println!("Pair: ({}, {})", pair.first, pair.second);
}

Output:

Pair: (Hello, 42)

Generic Methods in Structs

You can define methods with generics for structs, even if the struct itself is not generic.

Example:

struct Calculator;

impl Calculator {
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
a + b
}
}

fn main() {
println!("Sum of integers: {}", Calculator::add(5, 10));
println!("Sum of floats: {}", Calculator::add(2.5, 3.5));
}

Output:

Sum of integers: 15
Sum of floats: 6.0

Monomorphization in Rust

Monomorphization is the process by which Rust replaces generic types with concrete types at compile time. This ensures no runtime overhead for using generics.

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.

Leave a Comment

BoxofLearn