Generics
Generics let one definition work with many types. A generic definition uses a
type parameter such as T instead of naming one concrete type.
fn identity<T>(value: T) -> T {
value
}
fn main() {
let number = identity(42);
let text = identity("hello");
println!("{number} {text}");
}
The <T> after the function name declares a type parameter. Within the
function, T stands for one concrete type selected for each call.
identity(42) uses T = i32
identity("hello") uses T = &str
The two calls use the same function definition, but T is consistent within
each call. If the input is an i32, the output is also an i32.
Type parameters express relationships
The useful part of a generic signature is often the relationship between its positions:
fn choose<T>(first: T, second: T, use_first: bool) -> T {
if use_first {
first
} else {
second
}
}
fn main() {
let selected = choose("left", "right", true);
println!("{selected}");
}
Both arguments and the return value use T. The function requires the two
arguments to have the same type and promises to return that type.
This call fails because the arguments have different types:
fn choose<T>(first: T, second: T, use_first: bool) -> T {
if use_first { first } else { second }
}
fn main() {
let selected = choose(10, "ten", true);
}
Rust cannot choose one T that is both an integer and &str.
Use more than one parameter when the types may differ:
fn pair<T, U>(first: T, second: U) -> (T, U) {
(first, second)
}
fn main() {
let value = pair("level", 3);
println!("{}: {}", value.0, value.1);
}
Here T is &str and U is i32.
Generic functions cannot do everything
The body of an unconstrained generic function knows almost nothing about T:
#![allow(unused)]
fn main() {
fn print_value<T>(value: T) {
println!("{value}");
}
}
Not every possible T implements display formatting, so Rust rejects the
definition. The function must state that requirement with a trait bound:
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("{value}");
}
fn main() {
print_value(42);
print_value("hello");
}
Read T: Display as “any type T that implements Display.” The next chapter
covers traits and bounds in detail.
Generic structs
A struct can be generic over the types stored in its fields:
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 2, y: 5 };
let decimal = Point { x: 1.5, y: 3.0 };
println!("{integer:?}");
println!("{decimal:?}");
}
Point<i32> and Point<f64> are different concrete types produced from the
same definition. Because both fields use T, x and y must have the same
type in each value.
Use separate parameters when the fields may differ:
struct Pair<T, U> {
first: T,
second: U,
}
fn main() {
let pair = Pair {
first: "height",
second: 180,
};
println!("{}: {}", pair.first, pair.second);
}
Generic methods
Place the generic declaration after impl when defining methods for every
Point<T>:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let point = Point { x: 2, y: 5 };
println!("{}", point.x());
}
The first <T> declares the parameter. The T in Point<T> uses it:
impl<T> Point<T>
▲ ▲
declare use
A method can introduce additional type parameters of its own:
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
fn replace_second<V>(self, value: V) -> Pair<T, V> {
Pair {
first: self.first,
second: value,
}
}
}
fn main() {
let pair = Pair {
first: "level",
second: 3,
};
let pair = pair.replace_second("three");
println!("{}: {}", pair.first, pair.second);
}
The method consumes Pair<T, U> and returns Pair<T, V>, preserving the first
field’s type while changing the second.
Methods can target one concrete version
Not every method must exist for every T:
struct Point<T> {
x: T,
y: T,
}
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let point = Point { x: 3.0, y: 4.0 };
println!("{}", point.distance_from_origin());
}
distance_from_origin is available on Point<f64>, where the required
floating-point operations are known. It is not available on Point<String>.
Trait bounds provide a more flexible way to make methods available whenever a type supports the required operations.
Generic enums
Standard-library enums use generics extensively:
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
}
Option<T> represents either one value of type T or no value. Result<T, E>
represents either a success of type T or an error of type E.
The generic parameters let these enums describe many concrete types:
fn main() {
let port: Option<u16> = Some(8080);
let name: Option<&str> = None;
let parsed: Result<i32, _> = "42".parse();
println!("{port:?} {name:?} {parsed:?}");
}
Type inference usually fills in the arguments
Rust often infers generic arguments from function inputs or expected output:
fn main() {
let numbers: Vec<i32> = (1..=3).collect();
println!("{numbers:?}");
}
collect can build many collection types. The annotation Vec<i32> tells it
which one to produce.
You can also supply a generic argument explicitly with ::<>, informally
called the turbofish:
fn main() {
let numbers = (1..=3).collect::<Vec<i32>>();
println!("{numbers:?}");
}
Prefer inference when the surrounding code already makes the type clear. Use an annotation or turbofish when Rust needs help or when the type is useful to a reader.
Const generics parameterize values
Const generics use a compile-time value instead of a type. This function accepts a reference to an array of any length:
fn length<T, const N: usize>(_: &[T; N]) -> usize {
N
}
fn main() {
println!("{}", length(&[10, 20, 30]));
println!("{}", length(&['a', 'b']));
}
T is a type parameter. N is a usize value known at compile time. The array
length is part of the array’s type, so [i32; 3] and [i32; 4] are distinct
types.
Const generics are useful for fixed-size arrays and data structures whose size is part of their contract.
Generics have no inherent runtime type lookup
Rust normally compiles generic code through monomorphization. The compiler
generates concrete code for the versions the program uses, such as
identity::<i32> and identity::<&str>.
This provides static type checking and usually avoids runtime dispatch. The tradeoff is that many concrete versions can increase compile time and binary size.
Generics are most useful when the same operation is genuinely valid across multiple types. If only one type makes sense, a generic parameter adds complexity without adding flexibility.