Generics are type parameters for code reuse without fixing it to a concrete type.
You’ve probably noticed that you’ve been using generics all along.
Option<T>Result<T, E>Vec<T>HashMap<K, V>
Those uppercase letters (T, E, K, V) are Generics. They are placeholders that say: “A data type will go here, but I don’t know which one yet. You’ll tell me when you use the code”.
In C++ they are called Templates. In Java and C#, Generics. The idea is the same: avoid code duplication.
The Problem of Duplication
Imagine we want a function that prints a value and returns it (an identity function).
If we want it to work for numbers and strings, without generics we would have to do this:
fn identity_i32(x: i32) -> i32 {
println!("The value is: {}", x);
x
}
fn identity_string(x: String) -> String {
println!("The value is: {}", x);
x
}The body of the function is identical. Only the type changes. If this happened many times, it would become unsustainable.
Generics in Functions
To make it generic, we define an abstract type, which by convention we usually call T (for Type), between angle brackets < > right after the function name.
// We declare T We use T We return T
// | | |
// v v v
fn identity<T>( x: T) -> T {
// println!("{}", x); // ⚠️ Warning: This would error (explained below)
x
}
fn main() {
let number = identity(5); // T is i32
let text = identity("Hello"); // T is &str
}Why might println! fail?
If you try to print x inside the generic function, the compiler will complain: T doesn’t implement Display.
Rust is very safe. Since T can be “anything” (even a type that cannot be printed), Rust won’t let you assume anything about it. To solve this, we’ll need Trait Bounds, which we’ll cover in two articles.
Generics in Structs
Structs are one of the places where you’ll use them most often. Suppose you want to represent a coordinate point, but sometimes you need integer precision (pixels) and other times floating-point (vector graphics).
struct Point<T> {
x: T,
y: T,
}
fn main() {
// T is inferred as i32
let integer = Point { x: 5, y: 10 };
// T is inferred as f64
let floating = Point { x: 1.0, y: 4.0 };
}Multiple Generic Types
Notice that in the previous example, x and y must be the same type T.
If you try to do Point { x: 5, y: 4.0 }, it will fail.
If we need them to be different types, we declare multiple generics:
struct HybridPoint<T, U> {
x: T,
y: U,
}
fn main() {
let p = HybridPoint { x: 5, y: 4.0 }; // T=i32, U=f64
}Generics in Methods (impl)
When implementing methods for a generic struct, the syntax might seem a bit strange at first. We must declare <T> right after impl so Rust knows we are talking about a generic type, not a concrete struct named “T.”
struct Point<T> {
x: T,
y: T,
}
// "For any type T, we implement methods on Point<T>"
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}impl Blocks for Concrete Types
Rust lets us do something cool: implement methods only for a specific type.
// These methods exist for ANY Point
impl<T> Point<T> {
fn new(x: T, y: T) -> Self { Self { x, y } }
}
// These methods ONLY exist if T is f32
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p_int = Point { x: 5, y: 10 };
// p_int.distance_from_origin(); // ❌ Error: doesn't exist for i32
let p_float = Point { x: 3.0, y: 4.0 };
println!("{}", p_float.distance_from_origin()); // ✅ 5.0
}Performance and Monomorphization
This is the million-dollar question: Do generics add dynamic dispatch by themselves? In typical use of Rust’s generics, no: the compiler generates concrete versions through monomorphization.
Rust uses a process called Monomorphization (mono = one, morph = form).
At compile time, Rust looks at all the places where you use Point<T>. If it sees you use Point<i32> and Point<f64>, the compiler internally copies and pastes the struct’s code twice, replacing T with the actual type.
The result can be optimized as if we had written specific versions by hand. There is no implicit dynamic dispatch layer, although generating more code can increase the binary size and affect cache.
- 🟢 Advantage: Maximum performance.
- 🟡 Disadvantage: The compiled binary can be slightly larger (because the code is duplicated for each distinct type you use).