ZetCode

Rust generics

last modified October 25, 2026

In this article we show how to write generic code in Rust.

Generics let us write functions and types that work with many different types. Instead of a concrete type such as i32, the code uses a type parameter, a placeholder written in angle brackets, usually named T. The concrete type is filled in when the code is used.

Generics remove duplication without losing type safety or speed. The standard library is built on them: Vec<T>, Option<T>, Result<T, E>, and HashMap<K, V> are all generic types. Generic code is usually combined with traits, which state what operations the type parameters must support.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q.

The problem

We start with code that needs generics.

main.rs
fn largest_i32(vals: &[i32]) -> i32 {
    let mut largest = vals[0];
    for &v in vals {
        if v > largest {
            largest = v;
        }
    }
    largest
}

fn largest_f64(vals: &[f64]) -> f64 {
    let mut largest = vals[0];
    for &v in vals {
        if v > largest {
            largest = v;
        }
    }
    largest
}

fn main() {
    println!("{}", largest_i32(&[4, -2, 17, 9]));
    println!("{}", largest_f64(&[1.5, 0.3, 2.25]));
}

The two functions find the largest value in a slice. Their bodies are identical; only the types differ.

fn largest_i32(vals: &[i32]) -> i32 {

Supporting another type, for instance char, would require yet another copy of the same code.

$ cargo run -q
17
2.25

A generic function

A generic function declares its type parameters in angle brackets after its name.

main.rs
fn largest<T: PartialOrd + Copy>(vals: &[T]) -> T {
    let mut largest = vals[0];
    for &v in vals {
        if v > largest {
            largest = v;
        }
    }
    largest
}

fn main() {
    println!("{}", largest(&[4, -2, 17, 9]));
    println!("{}", largest(&[1.5, 0.3, 2.25]));
    println!("{}", largest(&['q', 'z', 'a']));
    println!("{}", largest(&[3u8, 250, 99]));
}

The single largest function replaces both copies.

fn largest<T: PartialOrd + Copy>(vals: &[T]) -> T {

The function is generic over a type T. Without the bounds, the compiler would reject the body, because it cannot assume anything about T. The PartialOrd bound allows comparing values with >, and Copy allows copying values out of the slice into largest.

println!("{}", largest(&['q', 'z', 'a']));

The type parameter is inferred from the argument. The function now works with integers, floats, characters, and any other type that satisfies the bounds.

$ cargo run -q
17
2.25
z
250

Returning a reference

The Copy bound excludes types such as String. By returning a reference, the function works with any comparable type.

main.rs
fn largest<T: PartialOrd>(vals: &[T]) -> Option<&T> {
    let mut iter = vals.iter();
    let mut largest = iter.next()?;
    for v in iter {
        if v > largest {
            largest = v;
        }
    }
    Some(largest)
}

fn main() {
    let words = vec![String::from("sky"), String::from("water"), String::from("rock")];
    println!("{:?}", largest(&words));

    let empty: [i32; 0] = [];
    println!("{:?}", largest(&empty));

    println!("{:?}", largest(&[0.5, 7.25, 3.0]));
}

The improved version also handles empty slices.

fn largest<T: PartialOrd>(vals: &[T]) -> Option<&T> {
    let mut iter = vals.iter();
    let mut largest = iter.next()?;

The function returns a reference into the slice, so nothing needs to be copied. The previous version panicked on vals[0] for an empty slice; this one returns None.

Strings are compared lexicographically, so "water" is the largest of the three words.

$ cargo run -q
Some("water")
None
Some(7.25)

Generic structs

Structs can have type parameters for their fields.

main.rs
#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

#[derive(Debug)]
struct Pair<K, V> {
    key: K,
    value: V,
}

fn main() {
    let p1 = Point { x: 5, y: 10 };
    let p2 = Point { x: 1.5, y: -0.5 };
    let p3: Point<i64> = Point { x: 1, y: 2 };

    println!("{:?}", p1);
    println!("{:?}", p2);
    println!("{} {}", p3.x, p3.y);

    let pr = Pair { key: "port", value: 8080 };
    let pr2 = Pair { key: 1, value: vec!['a', 'b'] };

    println!("{} = {}", pr.key, pr.value);
    println!("{} = {:?}", pr2.key, pr2.value);
}

We define a generic point and a generic key-value pair.

struct Point<T> {
    x: T,
    y: T,
}

Both fields have the same type T. A Point { x: 5, y: 1.5 } would not compile, because the two values would need different types.

struct Pair<K, V> {
    key: K,
    value: V,
}

With two type parameters, the fields can have different types.

let p3: Point<i64> = Point { x: 1, y: 2 };

The type is normally inferred from the values; integer literals default to i32. An annotation selects a different concrete type.

$ cargo run -q
Point { x: 5, y: 10 }
Point { x: 1.5, y: -0.5 }
1 2
port = 8080
1 = ['a', 'b']

Generic impl blocks

Methods of a generic struct are defined in a generic impl block. We can also add methods only for specific concrete types.

main.rs
#[derive(Debug, Clone, Copy)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }

    fn x(&self) -> &T {
        &self.x
    }

    fn swap(self) -> Point<T> {
        Point { x: self.y, y: self.x }
    }
}

impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

fn main() {
    let p = Point::new(3.0, 4.0);
    println!("x: {}", p.x());
    println!("distance: {}", p.distance_from_origin());
    println!("{:?}", p.swap());

    let q = Point::new('a', 'b');
    println!("{:?}", q.swap());
}

The Point<T> struct has methods for all types and one method only for f64.

impl<T> Point<T> {

The T must be declared after impl so that Rust knows Point<T> refers to a type parameter and not to a concrete type named T.

impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

This block applies only to Point<f64>. The sqrt method exists for floats, so the distance cannot be defined for all T. Calling distance_from_origin on a Point<char> is a compile-time error.

$ cargo run -q
x: 3
distance: 5
Point { x: 4.0, y: 3.0 }
Point { x: 'b', y: 'a' }

Generic enums

Enums can be generic as well, like Option<T> and Result<T, E> from the standard library.

main.rs
#[derive(Debug)]
enum Either<L, R> {
    Left(L),
    Right(R),
}

fn parse(s: &str) -> Either<i64, String> {
    match s.parse::<i64>() {
        Ok(n) => Either::Left(n),
        Err(_) => Either::Right(s.to_uppercase()),
    }
}

fn main() {
    let items = ["12", "sky", "-7", "blue"];

    for it in items {
        match parse(it) {
            Either::Left(n) => println!("number: {}", n * 2),
            Either::Right(s) => println!("text: {s}"),
        }
    }

    let v: Vec<Either<i64, String>> = items.iter().map(|s| parse(s)).collect();
    println!("{:?}", v);
}

The Either enum holds a value of one of two types.

enum Either<L, R> {
    Left(L),
    Right(R),
}

The enum is generic over the types of both variants.

fn parse(s: &str) -> Either<i64, String> {

The function fixes the type parameters to i64 and String. It returns a number when the text can be parsed and the uppercased text otherwise.

$ cargo run -q
number: 24
text: SKY
number: -14
text: BLUE
[Left(12), Right("SKY"), Left(-7), Right("BLUE")]

A generic collection

Generic types are the natural way to write containers.

main.rs
#[derive(Debug, Default)]
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 len(&self) -> usize {
        self.items.len()
    }

    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

fn main() {
    let mut nums = Stack::new();
    nums.push(1);
    nums.push(2);
    nums.push(3);

    println!("peek: {:?}", nums.peek());
    println!("len: {}", nums.len());

    let mut popped = Vec::new();
    while let Some(n) = nums.pop() {
        popped.push(n);
    }
    println!("popped: {:?}", popped);
    println!("empty: {}", nums.is_empty());

    let mut words: Stack<&str> = Stack::new();
    words.push("sky");
    words.push("cloud");
    println!("{:?}", words);
}

We implement a simple stack on top of a vector.

struct Stack<T> {
    items: Vec<T>,
}

The stack stores elements of any type. The methods need no trait bounds, because they only move elements in and out and never inspect them.

fn peek(&self) -> Option<&T> {
    self.items.last()
}

The peek method returns a reference to the top element without removing it.

let mut nums = Stack::new();
nums.push(1);

The element type is inferred from the first push. For the second stack, we write it explicitly as Stack<&str>.

$ cargo run -q
peek: Some(3)
len: 3
popped: [3, 2, 1]
empty: true
Stack { items: ["sky", "cloud"] }

Where clauses

A where clause moves the trait bounds after the signature, which helps when there are several type parameters with several bounds each.

main.rs
use std::collections::HashMap;
use std::fmt::Display;
use std::hash::Hash;

fn frequencies<T>(items: &[T]) -> HashMap<&T, usize>
where
    T: Eq + Hash,
{
    let mut map = HashMap::new();
    for it in items {
        *map.entry(it).or_insert(0) += 1;
    }
    map
}

fn print_sorted<K, V>(map: &HashMap<K, V>)
where
    K: Ord + Display,
    V: Display,
{
    let mut entries: Vec<(&K, &V)> = map.iter().collect();
    entries.sort_by(|a, b| a.0.cmp(b.0));
    for (k, v) in entries {
        println!("{k}: {v}");
    }
}

fn main() {
    let words = ["sky", "rock", "sky", "falcon", "rock", "sky"];
    let freq = frequencies(&words);
    print_sorted(&freq);

    let nums = [3, 1, 3, 3, 2, 1];
    print_sorted(&frequencies(&nums));
}

We count word frequencies with a generic function and print them sorted.

fn frequencies<T>(items: &[T]) -> HashMap<&T, usize>
where
    T: Eq + Hash,

Keys of a HashMap must implement Eq and Hash. The map stores references to the items, so the items are not copied.

fn print_sorted<K, V>(map: &HashMap<K, V>)
where
    K: Ord + Display,
    V: Display,

To sort the entries, the key type must implement Ord; both types must be printable. In main, K is &&str for the words and &i32 for the numbers. Shared references implement the same comparison and formatting traits as the values they point to.

$ cargo run -q
falcon: 1
rock: 2
sky: 3
1: 2
2: 1
3: 3

The turbofish

When the type parameter cannot be inferred, it is specified with the turbofish syntax ::<>.

main.rs
use std::collections::HashSet;

fn zero<T: Default>() -> T {
    T::default()
}

fn main() {
    let a = "42".parse::<i32>().unwrap();
    let b: f64 = "2.5".parse().unwrap();
    println!("{a} {b}");

    let letters = "hello".chars().collect::<HashSet<_>>();
    println!("unique letters: {}", letters.len());

    let n = zero::<u8>();
    let s = zero::<String>();
    let v: Vec<bool> = zero();
    println!("{n} {s:?} {v:?}");

    let total = [1, 2, 3].iter().sum::<i32>();
    println!("{total}");
}

Some functions are generic only in their return type, so the compiler needs to be told which type we want.

let a = "42".parse::<i32>().unwrap();
let b: f64 = "2.5".parse().unwrap();

The target type of parse can be given with the turbofish or with a type annotation on the variable. Both forms are equivalent.

let letters = "hello".chars().collect::<HashSet<_>>();

The _ placeholder lets the compiler infer the element type while we choose the collection.

fn zero<T: Default>() -> T {
    T::default()
}

Our own function generic in its return type works the same way: zero::<u8>() returns 0 and zero::<String>() returns an empty string.

$ cargo run -q
42 2.5
unique letters: 4
0 "" []
6

Const generics

Besides types, generics can be parameterized by constant values. This is mostly used for array lengths.

main.rs
fn sum<const N: usize>(arr: [i32; N]) -> i32 {
    arr.iter().sum()
}

#[derive(Debug)]
struct Buffer<const N: usize> {
    data: [u8; N],
    len: usize,
}

impl<const N: usize> Buffer<N> {
    fn new() -> Self {
        Buffer { data: [0; N], len: 0 }
    }

    fn push(&mut self, b: u8) -> bool {
        if self.len == N {
            return false;
        }
        self.data[self.len] = b;
        self.len += 1;
        true
    }

    fn capacity(&self) -> usize {
        N
    }
}

fn main() {
    println!("{}", sum([1, 2, 3]));
    println!("{}", sum([10; 5]));

    let mut buf: Buffer<3> = Buffer::new();
    for b in [7, 8, 9, 10] {
        let ok = buf.push(b);
        println!("push {b}: {ok}");
    }
    println!("{:?}, capacity {}", buf, buf.capacity());
}

We write a function that accepts arrays of any length and a buffer with a fixed capacity.

fn sum<const N: usize>(arr: [i32; N]) -> i32 {

The N parameter is a constant of type usize. [i32; 3] and [i32; 5] are different types, and the function works with both.

struct Buffer<const N: usize> {
    data: [u8; N],
    len: usize,
}

The capacity of the buffer is part of its type. The data lives in an array inside the struct, so no heap allocation is needed.

let mut buf: Buffer<3> = Buffer::new();

We create a buffer for three bytes. The fourth push fails.

$ cargo run -q
6
50
push 7: true
push 8: true
push 9: true
push 10: false
Buffer { data: [7, 8, 9], len: 3 }, capacity 3

Conditional methods

An impl block with trait bounds adds methods only for type parameters that satisfy them.

main.rs
use std::fmt::Display;

struct Pair<T> {
    a: T,
    b: T,
}

impl<T> Pair<T> {
    fn new(a: T, b: T) -> Self {
        Pair { a, b }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn show_largest(&self) {
        if self.a >= self.b {
            println!("largest: {}", self.a);
        } else {
            println!("largest: {}", self.b);
        }
    }
}

struct Token;

fn main() {
    let p1 = Pair::new(4, 9);
    p1.show_largest();

    let p2 = Pair::new("falcon", "eagle");
    p2.show_largest();

    let _p3 = Pair::new(Token, Token);
}

Every Pair<T> can be created, but only some of them can show the larger value.

impl<T: Display + PartialOrd> Pair<T> {
    fn show_largest(&self) {

The show_largest method exists only when T can be compared and printed.

let _p3 = Pair::new(Token, Token);

A Pair<Token> is a valid value, but _p3.show_largest() would not compile, because Token implements neither trait.

$ cargo run -q
largest: 9
largest: falcon

Monomorphization

Generics in Rust have no runtime cost. The compiler generates a separate copy of generic code for each concrete type it is used with. The process is called monomorphization.

main.rs
use std::any::type_name;

fn show<T: std::fmt::Debug>(val: T) {
    println!("{:<28} {:?}", type_name::<T>(), val);
}

fn main() {
    show(5);
    show(5u8);
    show(2.5);
    show("text");
    show(vec![1, 2]);
    show(Some('c'));
}

We print the concrete type that T stands for in each call.

println!("{:<28} {:?}", type_name::<T>(), val);

The std::any::type_name function returns the name of a type as a string. Each call of show is compiled into its own function, specialized for i32, u8, f64, and so on. The result is as fast as hand-written code for each type; the price is longer compile times and larger binaries when generic code is used with many types. When that matters, trait objects offer the opposite trade-off.

$ cargo run -q
i32                          5
u8                           5
f64                          2.5
&str                         "text"
alloc::vec::Vec<i32>         [1, 2]
core::option::Option<char>   Some('c')

Generic closures

Each closure has its own anonymous type. Functions that accept or return closures are therefore generic.

main.rs
fn apply_all<T, F>(items: &mut [T], f: F)
where
    F: Fn(&T) -> T,
{
    for it in items.iter_mut() {
        *it = f(it);
    }
}

fn compose<A, B, C>(f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> impl Fn(A) -> C {
    move |x| g(f(x))
}

fn main() {
    let mut nums = [1, 2, 3, 4];
    apply_all(&mut nums, |n| n * n);
    println!("{:?}", nums);

    let mut words = [String::from("sky"), String::from("sea")];
    apply_all(&mut words, |w| w.to_uppercase());
    println!("{:?}", words);

    let len_doubled = compose(|s: &str| s.len(), |n| n * 2);
    println!("{}", len_doubled("falcon"));
}

We apply a closure to every element of a slice and compose two closures.

fn apply_all<T, F>(items: &mut [T], f: F)
where
    F: Fn(&T) -> T,

The function is generic over the element type T and the closure type F. The bound says that F is something callable that takes &T and returns T.

fn compose<A, B, C>(f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> impl Fn(A) -> C {
    move |x| g(f(x))
}

The compose function returns a new closure that applies f and then g. Three type parameters describe the input, the intermediate value, and the output.

$ cargo run -q
[1, 4, 9, 16]
["SKY", "SEA"]
12

Source

The Rust Programming Language: Generic Data Types

The Rust Reference: Generic parameters

In this article we have worked with generics in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.