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