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



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:

100%
graph LR
    A["Generic Functions<br/>fn identity&lt;T&gt;(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(&#34;hello&#34;.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:

RUST
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

RUST
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

100%
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:

TEXT 📖 Display only
i32 Maximum value: <find_max(&numbers)>
f64 Maximum value: <find_max(&floats)>
&str Maximum value: <find_max(&strings)>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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, T is 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 infers T based on the actual argument type.


▶ Example 2: Generic Struct—Point Coordinate System (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
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()>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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; x and y are of the same type (both T). impl<T> Point<T> implements common methods for all T instances. 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:

TEXT 📖 Display only
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>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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 parameter T (with or without a value), while Result<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:

TEXT 📖 Display only
Key: <self.key>, Value: <self.value>
<mix_and_match(42, "answer")>
<mix_and_match(3.14, 100)>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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. The where clause is used to constrain the conditions that type parameters must satisfy. During monomorphization, the compiler generates separate code for each combination, such as Pair<String, i32> and Pair<&str, f64>.


▶ Example 5: Comprehensive Exercise—Generic Containers and Algorithms (Difficulty ⭐⭐⭐)

Output:

TEXT 📖 Display only
<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
RUST
// ============================================
// 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:

TEXT 📖 Display only
=== 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; the impl<T: Display> constraint ensures that the print_all method is available only when the type implements Display; use the PartialOrd constraint to implement the generic comparison operator.


❓ FAQ

Q What is the difference between generics and Box<dyn Any>?
A Generics determine types at compile time (static dispatch), while 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.
Q Does monomorphization result in larger binary files?
A Yes, but the impact is usually minimal. A separate code file is generated for each combination of types actually used. If a generic function is called with a large number of different types, the binary size will increase. However, the Rust compiler performs optimizations, and “code size” is generally not a bottleneck on modern CPUs. If size is a concern, consider using dyn Trait instead.
Q What is the difference between generic functions and generic methods?
A Generic functions are standalone functions, while generic methods are functions defined on types. Generic functions: 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.
Q What is the difference between the impl<T> and impl blocks?
A 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>.
Q What is the difference between the where clause for trait bounds and writing them directly in angle brackets?
A They serve the same purpose, but the 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


📝 Exercises

  1. Difficulty ⭐: Write a generic function fn echo<T>(x: T) -> T that takes a value and returns it as-is. In the main function, call it with i32, f64, and &str, respectively.
  2. Difficulty ⭐⭐: Define a generic struct Container<T> that has a field value: T. The implementation fn get(&self) -> &T returns a reference to the value and fn set(&mut self, val: T) modifies the value. Test it in the main function using Container<String> and Container<i32>, respectively.
  3. 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 implement Clone. Then, in the main function, merge two i32 slices and two &str slices, respectively.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏