Introduction: What are Generics and Why Are They in Rust?
\nImagine you need to write a function that finds the largest element in a list. For integers — one function, for floating-point numbers — another, for strings — a third. The code will be duplicated, and maintaining it will become difficult. In Rust, this problem is solved by Generics (generic programming).
\nGenerics allow you to write code that works with different data types without losing strict typing. Instead of duplicating functions for each type, you write one generic version. At compile time, Rust substitutes the concrete type and checks for correctness. This provides:
\n- \n
- Code Reusability — one function for all types. \n
- Safety — all type errors are caught at compile time. \n
- Performance — thanks to monomorphization (the compiler generates separate code for each used type), generic code runs as fast as specialized code. \n
In this article, we will explore how to use Generics in Rust with practical examples, starting from simple functions and moving on to structs and traits.
\n\n1. Generics in Functions: The First Step to Versatility
\nSyntax and Basic Example
\nTo make a function generic, you need to specify a type parameter in angle brackets <T>. Uppercase letters are typically used: T, U, V. Here is what a function that returns the passed value looks like:
fn identity<T>(value: T) -> T {\n value\n}\n\nfn main() {\n let num = identity(42);\n let text = identity("Hello");\n println!("{} {}", num, text); // Outputs: 42 Hello\n}\nThe compiler infers the type based on the passed argument. You can also specify the type explicitly: identity::<i32>(42).
Constraints: Traits as Type Bounds
\nWhat if we want to compare values? Simply writing value1 > value2 won't work — not all types support comparison. You need to specify that the type must implement the PartialOrd trait:
fn largest<T: PartialOrd>(list: &[T]) -> &T {\n let mut largest = &list[0];\n for item in list {\n if item > largest {\n largest = item;\n }\n }\n largest\n}\n\nfn main() {\n let numbers = vec![34, 50, 25, 100, 65];\n println!("Largest number: {}", largest(&numbers));\n\n let chars = vec!['y', 'm', 'a', 'q'];\n println!("Largest character: {}", largest(&chars));\n}\nHere T: PartialOrd is a trait bound. It guarantees that the comparison operator is defined for type T. Without this constraint, the code will not compile.
2. Generics in Structs and Enums
\nStructs with Generic Fields
\nGenerics work great with custom types too. For example, a Point struct can store coordinates of any type:
struct Point<T> {\n x: T,\n y: T,\n}\n\nfn main() {\n let integer_point = Point { x: 5, y: 10 };\n let float_point = Point { x: 1.0, y: 4.0 };\n // let mixed = Point { x: 5, y: 4.0 }; // Error! Both fields must be the same type\n}\nIf you need the fields to be of different types, use multiple parameters:
\nstruct Point<T, U> {\n x: T,\n y: U,\n}\n\nfn main() {\n let mixed = Point { x: 5, y: 4.0 }; // Now it works\n}\n\nEnums with Generics
\nThe standard Option<T> enum is an excellent example of a generic enum. You can create your own:
enum Result<T, E> {\n Ok(T),\n Err(E),\n}\n\n// Usage\nfn divide(a: f64, b: f64) -> Result<f64, String> {\n if b