Introduction: What are Generics and why are they needed in Rust?
Rust is a systems programming language known for its safety, performance, and strict type system. One of the key tools that allows writing flexible and reusable code is Generics.
Generics allow you to create functions, structs, enums, and methods that can work with different data types without needing to duplicate code. Instead of writing a separate function for i32, f64, or String, you write one generic version.
For example, without Generics you would have to create three different functions for sorting three different types. With Generics — just one. This not only reduces code volume but also increases its reliability: the compiler checks type correctness at compile time, not at runtime.
Main part: how Generics work in Rust
1. Generic functions: write once — use for any type
The simplest way to get acquainted with Generics is through generic functions. Imagine you need to write a function that returns the largest of two values. Instead of writing it for each type separately, use generics:
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest}
fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest(&numbers); println!("Largest number: {}", result);
let chars = vec!['y', 'm', 'a', 'q']; let result = largest(&chars); println!("Largest character: {}", result);}Here <T: PartialOrd> means the function works with any type T that supports comparison (the PartialOrd trait). This is a trait bound — an important concept that will be discussed further.
2. Generic structs and enums: flexibility in data definition
Generics apply not only to functions but also to data structures. A classic example is the Point struct, which can store coordinates of different types:
struct Point<T> { x: T, y: T,}
fn main() { let integer_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.0, y: 4.0 }; // let wont_work = Point { x: 5, y: 4.0 }; // Error! Different types}If you need x and y to be of different types, use multiple generic parameters:
struct Point<T, U> { x: T, y: U,}
fn main() { let mixed_point = Point { x: 5, y: 4.0 }; // Works!}The same works for enums. The most famous examples are Option<T> and Result<T, E>, which are generic:
enum Option<T> { Some(T), None,}
enum Result<T, E> { Ok(T), Err(E),}3. Methods in generic structs: adding logic
You can define methods for generic structs using an impl block. You can also create methods that work only for specific types:
struct Point<T> { x: T, y: T,}
// Methods for all types Timpl<T> Point<T> { fn x(&self) -> &T { &self.x }}
// Method only for type f64impl Point<f64> { fn distance_from_origin(&self) -> f64 { (self.x.powi(2) + self.y.powi(2)).sqrt() }}
fn main() { let p = Point { x: 3.0, y: 4.0 }; println!("p.x = {}", p.x()); println!("Distance from origin: {}", p.distance