Generics in Rust: why they are needed and how to use them in practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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_i32
  • print_type_f64
  • print_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

Blogs

Book Recommendations