Rust: Rust Generics: Type-Parameterized Programming
Last updated: 2026-08-26
Generics are "parameterized programming at the type level"—passing types as parameters so that the same code can be applied to multiple types without having to rewrite the code for each type.
If a function is "abstracting values into parameters," then generics are "abstracting types into parameters" as well. It’s like when you order takeout and say, "I’ll have a serving of rice," without specifying whether you want it as fried rice or rice over a dish—you decide that once you get to the restaurant.
1. What You'll Learn
- Definition and Calling of the Generic Function
fn foo<T>(x: T) - How to Use the Generic Struct
struct Point<T> - An In-Depth Understanding of Generic Enumerations
Option<T>andResult<T, E> - How to write a generic method
impl<T> - Monomorphization — a mechanism for compile-time expansion
- Using Multiple Type Parameters
2. Conceptual Diagrams
The following Mermaid diagram illustrates the mechanism by which the generic type parameter T is replaced with a concrete type during the monomorphization process at compile time:
graph LR
A["Generic Functions<br/>fn identity<T>(x: T) -> T"] --> B["Compile-Time Singletonization<br/>Monomorphization"]
B --> C["T → i32<br/>fn identity_i32(x: i32) -> i32"]
B --> D["T → String<br/>fn identity_string(x: String) -> String"]
B --> E["T → f64<br/>fn identity_f64(x: f64) -> f64"]
C --> F["Call identity(42)"]
D --> G["Call identity("hello".to_string())"]
E --> H["Call identity(3.14)"]
3. The Story of a Versatile Container
(1) The Pain: Writing Repetitive Code for Each Type
Luna (Luna) is developing a toolkit and needs a function to "find the maximum value."
At first, she wrote one for each type:
fn max_i32(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
fn max_f64(a: f64, b: f64) -> f64 {
if a > b { a } else { b }
}
fn max_str(a: &str, b: &str) -> &str {
if a > b { a } else { b }
}
fn main() {
println!("{}", max_i32(3, 7)); // 7
println!("{}", max_f64(2.5, 1.8)); // 2.5
println!("{}", max_str("apple", "banana")); // banana
}
Aside from their different types, the logic of these three functions is exactly the same. If there were also u32, u64, char, and so on, you’d have to copy the code for each additional type. This is “copy-and-paste programming”—neither elegant nor maintainable.
(2) Rust's Approach to Generics
fn max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", max(3, 7)); // 7
println!("{}", max(2.5, 1.8)); // 2.5
println!("{}", max("apple", "banana")); // banana
}
A generic function replaces three functions for specific types—and the compiler automatically generates specialized code for each type that is actually used (monomorphism). You write one, and the compiler expands it into many for you.
4. Core Concepts
(1) Generic System
graph TB
A[Generics Generics] --> B[Generic Functions]
A --> C[Generic Structures]
A --> D[Generic Enumerations]
A --> E[Generic Methods]
B --> F["fn identity<T>(x: T) -> T"]
C --> G["struct Point<T> { x: T, y: T }"]
D --> H["Option<T>, Result<T, E>"]
E --> I["impl<T> Point<T> { fn x(&self) -> &T }"]
A --> J[Singleton Monomorphization]
J --> K["At compile time:T → i32, f64, String ..."]
J --> L["Generate separate code for each type"]
(2) Generics vs. Concrete Types vs. Dynamic Dispatch
| Feature | Specific Type (Non-Generic) | Generic (Monomorphic) | Dynamic Dispatch (dyn Trait) |
|---|---|---|---|
| Code duplication | Write one copy for each type | Automatic expansion by the compiler | One copy of code, distributed at runtime |
| Performance | Best | Best (without virtual function overhead) | With virtual function overhead |
| Compilation Time | Long (lots of hand-written code) | Fairly long (many expansions) | Short |
| Binary Volume | Large | Medium (one copy per type) | Small |
| Flexibility | Poor | Determined at compile time | Determined at runtime |
(3) Common Generic Enumerations
| Enumeration | Definition | Purpose |
|---|---|---|
Option<T> |
enum Option<T> { Some(T), None } |
Values that may be empty |
Result<T, E> |
enum Result<T, E> { Ok(T), Err(E) } |
Operations That May Fail |
Vec<T> |
struct Vec<T> { ... } |
Dynamic Arrays |
HashMap<K, V> |
struct HashMap<K, V> { ... } |
Key-value mapping |
(4) Comparison of Generic Constraint Methods
| Constraint Type | Syntax | Use Cases | Example |
|---|---|---|---|
| Inline Constraints | fn foo<T: Trait>(x: T) |
Simple Single Constraints | fn max<T: PartialOrd>(a: T, b: T) |
| Multiple Constraints + | fn foo<T: Trait1 + Trait2>(x: T) |
Multiple Constraints | fn print<T: Display + Clone>(x: T) |
| WHERE clause | fn foo<T>(x: T) where T: Trait |
Complex constraints, multi-type parameters | where T: Display + Clone, U: Debug |
| impl Trait | fn foo(x: impl Trait) |
Shorthand Notation (Syntax Sugar) | fn plug(d: &impl USBDevice) |
5. Generic Examples
▶ Example 1: Generic Function—Find the Maximum Value in an Array (Difficulty ⭐)
Output:
i32 Maximum value: <find_max(&numbers)>
f64 Maximum value: <find_max(&floats)>
&str Maximum value: <find_max(&strings)>
// ============================================
// Generic Functions:Applies to any comparable type
// ============================================
// PartialOrd Constraint Assurance T Supports comparison operations
fn find_max<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list.iter() {
if item > max {
max = item;
}
}
max
}
fn main() {
let numbers = vec![3, 7, 1, 9, 4];
println!("i32 Maximum value: {}", find_max(&numbers));
let floats = vec![2.5, 1.8, 3.14, 0.99];
println!("f64 Maximum value: {}", find_max(&floats));
let strings = vec!["apple", "banana", "cherry", "date"];
println!("&str Maximum value: {}", find_max(&strings));
// The Same Function,Three Types,Compiler Auto-Expansion
}
Output:
int_point: <int_point>
float_point x: <float_point.x()>
string_point: <string_point>
Distance from the origin: <float_point.distance_from_origin()>
<int_point.distance_from_origin()>
find_max<T>is a generic function,Tis a type parameter, and<T: std::cmp::PartialOrd>is a trait bound—meaning "T must be a comparable type." When the function is called, the compiler automatically infersTbased on the actual argument type.
▶ Example 2: Generic Struct—Point Coordinate System (Difficulty ⭐⭐)
Output:
int_point: <int_point>
float_point x: <float_point.x()>
string_point: <string_point>
Distance from the origin: <float_point.distance_from_origin()>
<int_point.distance_from_origin()>
// ============================================
// Generic Structures:Point Can store coordinates of any type
// ============================================
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
// Implementation Methods for Generic Structures
impl<T> Point<T> {
// Back x Citation
fn x(&self) -> &T {
&self.x
}
// Back y Citation
fn y(&self) -> &T {
&self.y
}
}
// For Point of a specific type f64, implement additional methods
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let int_point = Point { x: 5, y: 10 };
let float_point = Point { x: 3.0, y: 4.0 };
let string_point = Point {
x: "left",
y: "right",
};
println!("int_point: {:?}", int_point);
println!("float_point x: {}", float_point.x());
println!("string_point: {:?}", string_point);
// Only `Point<f64>` has the distance_from_origin method
println!("Distance from the origin: {:.2}", float_point.distance_from_origin());
// Compilation Error: int_point is Point<i32>, no such method
// println!("{}", int_point.distance_from_origin());
}
Output:
int_point: Point { x: 5, y: 10 }
float_point x: 3.0
string_point: Point { x: "left", y: "right" }
Distance from the origin: 5.00
Point<T>is a generic struct;xandyare of the same type (bothT).impl<T> Point<T>implements common methods for allTinstances.impl Point<f64>implements specific methods only for certain types—this is one of the major advantages of generics.
▶ Example 3: Generic Enumerations—Option and Result in Practice (Difficulty ⭐⭐)
Output:
Found 30,Index: <index>
Not found
Found 99,Index: <index>
Not found 99
Parsing Successful: <n>
Parsing Failed: <e>
Parsing Successful: <n>
Parsing Failed: <e>
Custom Result: <result>
Custom Result: <result>
// ============================================
// Generic Enumerations: Option<T> and Result<T, E> Usage
// ============================================
// Custom Result Style Enumeration
#[derive(Debug)]
enum MyResult<T, E> {
Success(T),
Failure(E),
}
// Division Function:Back Result Style
fn safe_divide<T>(a: T, b: T) -> MyResult<T, String>
where
T: std::ops::Div<Output = T> + std::cmp::PartialEq + From<u8> + Copy,
{
if b == 0.into() {
MyResult::Failure("Division by zero".to_string())
} else {
MyResult::Success(a / b)
}
}
// Using the standard library Option<T>
fn find_in_vector<T: PartialEq>(vec: &[T], target: &T) -> Option<usize> {
for (i, item) in vec.iter().enumerate() {
if item == target {
return Some(i);
}
}
None
}
// Using the standard library Result<T, E>
fn parse_number(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|e| format!("Parse error: {}", e))
}
fn main() {
// Option Usage
let numbers = vec![10, 20, 30, 40, 50];
match find_in_vector(&numbers, &30) {
Some(index) => println!("Found 30,Index: {}", index),
None => println!("Not found"),
}
match find_in_vector(&numbers, &99) {
Some(index) => println!("Found 99,Index: {}", index),
None => println!("Not found 99"),
}
// Result Usage
match parse_number("42") {
Ok(n) => println!("Parsing Successful: {}", n),
Err(e) => println!("Parsing Failed: {}", e),
}
match parse_number("hello") {
Ok(n) => println!("Parsing Successful: {}", n),
Err(e) => println!("Parsing Failed: {}", e),
}
// Custom MyResult Usage
let result = safe_divide(10.0, 3.0);
println!("Custom Result: {:?}", result);
let result = safe_divide(10.0, 0.0);
println!("Custom Result: {:?}", result);
}
Output:
Found 30,Index: 2
Not found 99
Parsing Successful: 42
Parsing Failed: Parse error: invalid digit found in string
Custom Result: Success(3.3333333333333335)
Custom Result: Failure("Division by zero")
Option<T>has only one type parameterT(with or without a value), whileResult<T, E>has two type parameters (the success type and the error type). Generic enums allow these types to be applied to any data type—this is one of the core design principles of the Rust standard library.
▶ Example 4: Multiple Type Parameters and Monomorphism (Difficulty ⭐⭐⭐)
Output:
Key: <self.key>, Value: <self.value>
<mix_and_match(42, "answer")>
<mix_and_match(3.14, 100)>
// ============================================
// Multiple Type Parameters + Combining Generic Methods
// ============================================
use std::fmt::Display;
// Generic struct with two type parameters
#[derive(Debug)]
struct Pair<K, V> {
key: K,
value: V,
}
// Implement methods for Pair<K, V>
impl<K, V> Pair<K, V> {
fn new(key: K, value: V) -> Self {
Pair { key, value }
}
}
// Constrained methods: only available when both K and V implement Display
impl<K: Display, V: Display> Pair<K, V> {
fn print(&self) {
println!("Key: {}, Value: {}", self.key, self.value);
}
}
// Generic Methods:Mixing Different Types of Parameters
fn mix_and_match<T, U>(a: T, b: U) -> String
where
T: Display,
U: Display,
{
format!("Mixed: {} and {}", a, b)
}
fn main() {
// Multiple Type Parameters: String and i32
let pair1 = Pair::new("Age".to_string(), 25);
pair1.print();
// Multiple Type Parameters: &str and f64
let pair2 = Pair::new("PI", 3.14159);
pair2.print();
// Different Types of Combinations
let pair3 = Pair::new(100, "HTTP OK");
// pair3.print(); // ❌ Compilation Error: i32 and &str both implement Display, but no error here.
// In fact i32 and &str both implement Display, so you can call it
// This is just to demonstrate the concept of constraint methods.
pair3.print();
// Mixing Different Types
println!("{}", mix_and_match(42, "answer"));
println!("{}", mix_and_match(3.14, 100));
}
Output:
Key: Age, Value: 25
Key: PI, Value: 3.14159
Key: 100, Value: HTTP OK
Mixed: 42 and answer
Mixed: 3.14 and 100
Multiple type parameters (
<K, V>) allow a struct to hold data of different types. Thewhereclause is used to constrain the conditions that type parameters must satisfy. During monomorphization, the compiler generates separate code for each combination, such asPair<String, i32>andPair<&str, f64>.
▶ Example 5: Comprehensive Exercise—Generic Containers and Algorithms (Difficulty ⭐⭐⭐)
Output:
<item>
=== Integer Stack ===
Stack Contents:
Stack top: <int_stack.peek()>
Pop up: <int_stack.pop()>
Remaining <int_stack.len()> element
=== String Stack ===
=== Generic Search ===
Find 30: Index <find_first(&nums, &30)>
Find 99: Index <find_first(&nums, &99)>
Find 'banana': Index <find_first(&words, &"banana")>
=== Generic Swaps ===
Before the exchange: x=42, y=10
After the exchange: x=42, y=10
// ============================================
// Comprehensive Example:Generic Stack + Generic Search Algorithms
// ============================================
use std::fmt::Display;
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn len(&self) -> usize {
self.items.len()
}
}
impl<T: Display> Stack<T> {
fn print_all(&self) {
for item in &self.items {
print!("{} ", item);
}
println!();
}
}
fn find_first<T: PartialEq>(items: &[T], target: &T) -> Option<usize> {
items.iter().position(|x| x == target)
}
fn swap_if_greater<T: PartialOrd>(a: &mut T, b: &mut T) {
if *a > *b {
std::mem::swap(a, b);
}
}
fn main() {
let mut int_stack: Stack<i32> = Stack::new();
int_stack.push(10);
int_stack.push(20);
int_stack.push(30);
println!("=== Integer Stack ===");
println!("Stack Contents: ");
int_stack.print_all();
println!("Stack top: {:?}", int_stack.peek());
println!("Pop up: {:?}", int_stack.pop());
println!("Remaining {} element", int_stack.len());
let mut str_stack: Stack<&str> = Stack::new();
str_stack.push("Rust");
str_stack.push("is");
str_stack.push("awesome");
println!("\n=== String Stack ===");
str_stack.print_all();
let nums = vec![10, 20, 30, 40, 50];
println!("\n=== Generic Search ===");
println!("Find 30: Index {:?}", find_first(&nums, &30));
println!("Find 99: Index {:?}", find_first(&nums, &99));
let words = vec!["apple", "banana", "cherry"];
println!("Find 'banana': Index {:?}", find_first(&words, &"banana"));
let mut x = 42;
let mut y = 10;
println!("\n=== Generic Swaps ===");
println!("Before the exchange: x={}, y={}", x, y);
swap_if_greater(&mut x, &mut y);
println!("After the exchange: x={}, y={}", x, y);
}
Output:
=== Integer Stack ===
Stack Contents:
10 20 30
Stack top: Some(30)
Pop up: Some(30)
Remaining 2 element
=== String Stack ===
Rust is awesome
=== Generic Search ===
Find 30: Index Some(2)
Find 99: Index None
Find 'banana': Index Some(1)
=== Generic Swaps ===
Before the exchange: x=42, y=10
After the exchange: x=10, y=42
The generic type
Stack<T>works for both i32 and &str; theimpl<T: Display>constraint ensures that theprint_allmethod is available only when the type implements Display; use thePartialOrdconstraint to implement the generic comparison operator.
❓ FAQ
Box<dyn Any>?dyn Any determines types at runtime (dynamic dispatch). Generics offer better performance because the compiler generates specialized code for each type, eliminating the overhead of virtual functions. dyn Any is more flexible, as it can handle any type at runtime, but it incurs runtime overhead.dyn Trait instead.fn foo<T>(x: T). Generic methods: impl<T> MyType<T> { fn bar(&self) }. Generic methods can access the Self type, whereas generic functions cannot.impl<T> and impl blocks?impl<T> is the implementation method for all T types, while the standard impl is the implementation method for specific types. impl<T> Point<T> { fn x(&self) } is available for all Point types. impl Point<f64> { fn distance(&self) } is only available for Point<f64>.where clause for trait bounds and writing them directly in angle brackets?where clause offers better readability for complex constraints. fn foo<T: Display + Clone, U: Debug>(t: T, u: U) is equivalent to fn foo<T, U>(t: T, u: U) where T: Display + Clone, U: Debug. The where clause is recommended when there are multiple constraints.📖 Summary
- Generic functions
fn foo<T>(x: T)allow the same function to be applied to multiple types - Generic Structures
struct Point<T>allow structure fields to store any type - Generic enumerations
Option<T>andResult<T, E>are prime examples of generic design in the Rust standard library. - Generic methods
impl<T> Type<T>implement methods for all type parameters, and can also implement specific methods for particular types - Monomorphization is the process by which a compiler expands generic code into code for specific types at compile time—with zero runtime overhead.
- Multiple type parameters
<T, U>andwhereconstraint clauses make generics both flexible and safe
📝 Exercises
- Difficulty ⭐: Write a generic function
fn echo<T>(x: T) -> Tthat takes a value and returns it as-is. In themainfunction, call it withi32,f64, and&str, respectively. - Difficulty ⭐⭐: Define a generic struct
Container<T>that has a fieldvalue: T. The implementationfn get(&self) -> &Treturns a reference to the value andfn set(&mut self, val: T)modifies the value. Test it in the main function usingContainer<String>andContainer<i32>, respectively. - Difficulty ⭐⭐⭐: Write a generic function
fn merge_arrays<T>(a: &[T], b: &[T]) -> Vec<T>that merges two slices and returns a new Vec. The type T must implementClone. Then, in the main function, merge twoi32slices and two&strslices, respectively.