Generics in Rust: why they are needed and how to use them in practice
Generic programming (Generics) is one of the key features of Rust, which allows you to write flexible, reusable, and safe code. In this article, we will break down why Generics are needed, how they work under the hood, and how to apply them in real projects. The material will be useful both for beginners just learning Rust and for experienced developers looking to systematize their knowledge.
Why are Generics needed in Rust?
Imagine you need to write a function that finds the largest element in an array. Without Generics, you would have to create separate versions for each data type: i32, f64, String, and so on. This leads to code duplication and complicates maintenance.
Generics solve this problem by allowing you to parameterize types. Instead of writing:
fn find_max_i32(list: &[i32]) -> i32 { ... }fn find_max_f64(list: &[f64]) -> f64 { ... }You write one universal function:
fn find_max<T: PartialOrd>(list: &[T]) -> &T { ... }The main advantages of Generics:
- Code reuse — one function or structure works with different types
- Type safety at compile time — all type errors are detected before the program runs
- Performance — Rust uses monomorphization, creating optimized code for each specific type
- Readability — less duplication, the code becomes more concise
How Generics work: monomorphization
Rust compiles generic code in a special way — through monomorphization. This means the compiler creates a separate copy of a function or structure for each specific type used in the program.
Example:
fn print_type<T>(val: T) { println!("Value: {}", val);}
fn main() { print_type(42); // T = i32 print_type(3.14); // T = f64 print_type("Hello"); // T = &str}After compilation, Rust will create three versions of print_type:
print_type_i32print_type_f64print_type_str
This guarantees that generic code does not lag behind specialized code in performance. The only "downside" is an increase in the binary file size, but in practice this rarely becomes a problem.
Using Generics with functions and structures
Generics in functions
The simplest way to use Generics is to parameterize a function:
fn swap<T>(a: &mut T, b: &mut T) { let temp = std::mem::replace(a, unsafe { std::mem::zeroed() }); *a = std::mem::replace(b, temp); *b = temp;}
fn main() { let mut x = 10; let mut y = 20; swap(&mut x, &mut y); println!("x = {}, y = {}", x, y); // x = 20, y = 10}Note: we don't specify the type — the compiler infers it automatically based on the arguments.
Generics in structures
Structures can also be generic. This is especially useful for data containers:
struct Point<T> { x: T, y: T,}
impl<T> Point<T> { fn new(x: T, y: T) -> Self { Point { x, y } }
fn distance_from_origin(&self) -> f64 where T: Into<f64> + Copy, { let x: f64 = self.x.into(); let y: f64 = self.y.into(); (x * x + y * y).sqrt() }}
fn main() { let int_point = Point::new(3, 4); let float_point = Point::new(3.0, 4.0); // distance_from_origin works only for types implementing Into<f6