Generics in Rust: Why They Are Needed and How to Use Them

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

Introduction to Generics in Rust



Generic programming (generics) is one of the key concepts in Rust, allowing you to write flexible, reusable, and safe code. If you are transitioning to Rust from other languages (e.g., C++ or Java), you have likely encountered templates or generics. In Rust, generics work similarly, but with a special emphasis on performance and memory safety.



Why are generics needed? Imagine you need to write a function that finds the maximum element in a list. Without generics, you would have to write a separate version for i32, f64, String, and every custom type. With generics, you write one function that works with any type that supports comparison.



In this article, we will break down what generics are in Rust, how to use them with functions, structs, and traits, and also look at practical examples.



Basics of Generics: functions and structs



Generic Functions



The simplest way to get acquainted with generics is to write a generic function. The syntax uses angle brackets <T>, where T is the type parameter.



fn largest<T>(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);}


However, this code will not compile! Rust requires that the type T implements the PartialOrd trait (for comparison). We will look at trait bounds later, but for now remember: generics allow you to abstract from a concrete type, but the compiler must know which operations are allowed.



Generic Structs



Generics work not only with functions but also with custom data types. For example, a point with coordinates can store integers or floating-point numbers:



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 }; println!("Integer point: ({}, {})", integer_point.x, integer_point.y); println!("Floating point: ({}, {})", float_point.x, float_point.y);}


You can use multiple type parameters if the fields have different types:



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

fn main() { let pair = Pair { first: 42, second: "answer" }; println!("Pair: {} and {}", pair.first, pair.second);}


Generics with Traits and Trait Bounds



What are trait bounds?



Trait bounds are a way to tell the compiler that a generic type must implement certain traits. Without them, we cannot call methods on a generic value. Let's return to the largest function and fix it:



use std::cmp::PartialOrd;

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]; println!("Largest: {}", largest(&numbers));}


Now the function only accepts types that support comparison (PartialOrd).



Multiple Bounds



You can specify several traits using +. For example, a type must be both comparable and cloneable:



use std::fmt::Display;

fn print_and_compare

Blogs

Book Recommendations