Post

Rust Notes: Generics

A review of Generics in Rust.

Rust Notes: Generics

Generics in Rust

Generics are Rust’s mechanism for writing code that works with multiple types without duplicating the implementation.

Instead of writing:

1
2
3
fn largest_i32(list: &[i32]) -> &i32 {
    // ...
}

and another function for f64:

1
2
3
fn largest_f64(list: &[f64]) -> &f64 {
    // ...
}

we can write one generic function:

1
2
3
fn largest<T>(list: &[T]) -> &T {
    // ...
}

Here T is a generic type parameter.

Generics are one of the foundations of Rust’s type system and appear together with:

  • ownership
  • borrowing
  • lifetimes
  • traits
  • trait bounds
  • associated types
  • impl Trait
  • trait objects
  • const generics
  • static dispatch
  • monomorphization

The most important idea is:

Generics let us write code in terms of a type that will be specified later.


1. Why Do We Need Generics?

Imagine we want a function that returns the first element of a slice.

Without generics:

1
2
3
4
5
6
7
8
9
10
11
fn first_i32(values: &[i32]) -> &i32 {
    &values[0]
}

fn first_string(values: &[String]) -> &String {
    &values[0]
}

fn first_f64(values: &[f64]) -> &f64 {
    &values[0]
}

The implementations are identical.

Only the type changes.

Generics allow us to write:

1
2
3
fn first<T>(values: &[T]) -> &T {
    &values[0]
}

Now the same function works with:

1
2
3
4
5
6
7
8
let numbers = vec![1, 2, 3];
let strings = vec![
    String::from("hello"),
    String::from("world"),
];

let n = first(&numbers);
let s = first(&strings);

The compiler determines T from the arguments.


2. What Is T?

In:

1
2
3
fn first<T>(values: &[T]) -> &T {
    &values[0]
}

T is a type parameter.

Think of it as a placeholder:

1
2
3
T
│
└── some type

For example, when calling:

1
first(&numbers)

the compiler can infer:

1
T = i32

When calling:

1
first(&strings)

it can infer:

1
T = String

Conceptually:

1
2
first<i32>(&[i32])
first<String>(&[String])

3. Generic Syntax

The basic syntax is:

1
<T>

For example:

1
2
fn foo<T>(value: T) {
}

Multiple type parameters:

1
2
fn foo<T, U>(x: T, y: U) {
}

A generic struct:

1
2
3
4
struct Pair<T, U> {
    first: T,
    second: U,
}

A generic enum:

1
2
3
4
enum Result<T, E> {
    Ok(T),
    Err(E),
}

A generic trait:

1
2
trait Container<T> {
}

4. Generic Functions

The most common syntax is:

1
2
3
fn identity<T>(value: T) -> T {
    value
}

Usage:

1
2
let x = identity(10);
let y = identity("hello");

The compiler infers:

1
T = i32

for the first call and:

1
T = &str

for the second.


5. Explicit Generic Arguments

Sometimes you can specify the generic type explicitly.

For example:

1
2
3
fn identity<T>(value: T) -> T {
    value
}

You can write:

1
let x = identity::<i32>(10);

The syntax:

1
::<T>

is called turbofish syntax.

For multiple types:

1
2
3
4
5
fn pair<T, U>(x: T, y: U) -> (T, U) {
    (x, y)
}

let value = pair::<i32, String>(10, String::from("hello"));

6. Why Is It Called Turbofish?

This:

1
::<T>

is commonly called the turbofish.

For example:

1
foo::<i32>();

The :: followed by <...> makes the syntax visually resemble a fish.

You will frequently see it with generic methods and functions:

1
"42".parse::<i32>();

Here:

1
2
3
parse::<i32>()
       ↑
       explicitly choose T = i32

7. Type Inference

Rust usually does not require explicit generic arguments.

For example:

1
2
3
4
5
fn identity<T>(value: T) -> T {
    value
}

let x = identity(10);

Rust sees:

1
10 → i32

and therefore infers:

1
T = i32

You can explicitly specify it:

1
let x = identity::<i32>(10);

Both are valid.

Idiomatic Rust generally relies on inference when it is obvious.


8. Generic Structs

A struct can have generic type parameters:

1
2
3
4
struct Point<T> {
    x: T,
    y: T,
}

Now:

1
2
3
4
5
6
7
8
9
let integer_point = Point {
    x: 10,
    y: 20,
};

let float_point = Point {
    x: 1.5,
    y: 2.5,
};

Conceptually:

1
2
Point<i32>
Point<f64>

9. Different Types in the Same Struct

You can use multiple generic parameters:

1
2
3
4
struct Point<T, U> {
    x: T,
    y: U,
}

Now:

1
2
3
4
let point = Point {
    x: 10,
    y: 3.14,
};

Conceptually:

1
Point<i32, f64>

This is useful when the fields do not need to have the same type.


10. Generic Enums

Rust’s standard library uses generics extensively.

For example:

1
2
3
4
enum Option<T> {
    Some(T),
    None,
}

This allows:

1
2
3
Option<i32>
Option<String>
Option<&str>

For example:

1
2
3
let x: Option<i32> = Some(10);

let y: Option<String> = Some(String::from("hello"));

The enum itself does not need separate implementations for every type.


11. Result<T, E>

Another important generic type is:

1
Result<T, E>

It represents:

1
2
T → success value
E → error value

For example:

1
Result<String, std::io::Error>

or:

1
Result<i32, ParseIntError>

This is a powerful example of why generics are useful.

The same Result abstraction works with many combinations of success and error types.


12. Generic Methods

A struct can be generic and its methods can also be generic.

For example:

1
2
3
4
5
6
7
8
9
struct Container<T> {
    value: T,
}

impl<T> Container<T> {
    fn value(&self) -> &T {
        &self.value
    }
}

Notice:

1
impl<T> Container<T>

The first T declares the generic parameter.

The second T uses it.


13. Why Do We Need impl<T>?

Suppose:

1
2
3
struct Container<T> {
    value: T,
}

To implement methods for all T:

1
2
3
4
5
impl<T> Container<T> {
    fn value(&self) -> &T {
        &self.value
    }
}

Read it as:

For every type T, implement these methods for Container<T>.


14. Implementing Only for One Concrete Type

You can also specialize the implementation for one concrete type:

1
2
3
4
5
6
7
8
9
struct Container<T> {
    value: T,
}

impl Container<i32> {
    fn double(&self) -> i32 {
        self.value * 2
    }
}

Now double() exists only for:

1
Container<i32>

It does not exist for:

1
Container<String>

15. Generic Implementation with a Different Type Parameter

You can introduce additional generic parameters inside impl.

For example:

1
2
3
4
5
6
7
8
9
struct Container<T> {
    value: T,
}

impl<T> Container<T> {
    fn pair<U>(self, other: U) -> (T, U) {
        (self.value, other)
    }
}

Usage:

1
2
3
let c = Container { value: 10 };

let result = c.pair("hello");

The types are:

1
2
T = i32
U = &str

16. Generic Functions and Generic Methods Are Different

Consider:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Container<T> {
    value: T,
}

impl<T> Container<T> {
    fn get(&self) -> &T {
        &self.value
    }

    fn convert<U>(self, value: U) -> (T, U) {
        (self.value, value)
    }
}

Here:

1
T

belongs to the struct.

While:

1
U

belongs only to the method.


17. Generic Traits

Traits can also be generic:

1
2
3
trait Converter<T> {
    fn convert(&self) -> T;
}

A type can implement the trait for a particular T:

1
2
3
4
5
6
7
struct Number;

impl Converter<i32> for Number {
    fn convert(&self) -> i32 {
        42
    }
}

18. Trait Generic Parameters vs Associated Types

This is an important Rust distinction.

A generic trait:

1
2
3
trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

would allow one type to implement the trait multiple times for different T.

Rust’s actual Iterator uses an associated type:

1
2
3
4
5
trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

This means a particular implementation chooses one Item type.

For example:

1
2
3
4
5
6
7
impl Iterator for MyIterator {
    type Item = i32;

    fn next(&mut self) -> Option<Self::Item> {
        // ...
    }
}

19. Why Associated Types Matter

With:

1
2
3
trait Iterator {
    type Item;
}

we can write:

1
2
3
4
5
6
fn sum<I>(iter: I) -> i32
where
    I: Iterator<Item = i32>,
{
    // ...
}

The Item = i32 syntax constrains the associated type.

This is different from:

1
I: Iterator<i32>

because Iterator does not have a generic parameter.


20. Trait Bounds

A generic type can have restrictions.

For example:

1
2
3
fn print<T: std::fmt::Display>(value: T) {
    println!("{value}");
}

The:

1
T: Display

is a trait bound.

It says:

T must implement Display.

Without this bound, Rust cannot assume that T supports {value} formatting.


21. Why Do Generic Types Need Bounds?

Consider:

1
2
3
fn add<T>(a: T, b: T) -> T {
    a + b
}

This does not compile.

Why?

Because Rust cannot assume that every type supports:

1
+

For example:

1
2
3
4
String
File
TcpStream
MyCustomType

do not automatically have addition semantics.

We need to specify the required capability.


22. Using a Trait Bound

For a type that supports addition:

1
2
3
4
5
6
7
8
use std::ops::Add;

fn add<T>(a: T, b: T) -> T
where
    T: Add<Output = T>,
{
    a + b
}

Now the compiler knows that T implements Add.

This demonstrates an important idea:

Generics describe what can vary; trait bounds describe what the generic type must be able to do.


23. Multiple Trait Bounds

You can require multiple traits:

1
2
3
4
5
fn process<T>(value: T)
where
    T: Clone + std::fmt::Debug + Send,
{
}

This means T must implement:

1
2
3
Clone
Debug
Send

You can also write:

1
2
fn process<T: Clone + std::fmt::Debug + Send>(value: T) {
}

Both forms are valid.


24. where Clauses

When bounds become complicated, use where:

1
2
3
4
5
6
fn process<T, U>(x: T, y: U)
where
    T: Clone + Send,
    U: std::fmt::Debug,
{
}

This is often more readable than:

1
2
3
4
5
fn process<T: Clone + Send, U: std::fmt::Debug>(
    x: T,
    y: U,
) {
}

A good rule:

Use inline bounds for simple constraints and where for complex ones.


25. Bounds on Structs

You can put bounds directly on a struct declaration:

1
2
3
4
5
6
struct Container<T>
where
    T: Clone,
{
    value: T,
}

However, it is often better to put bounds on the specific impl or function that needs them.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
struct Container<T> {
    value: T,
}

impl<T> Container<T>
where
    T: Clone,
{
    fn duplicate(&self) -> T {
        self.value.clone()
    }
}

This keeps the struct usable with any T.


26. Bounds on impl

Very common:

1
2
3
4
5
6
7
8
impl<T> Container<T>
where
    T: Clone,
{
    fn duplicate(&self) -> T {
        self.value.clone()
    }
}

The implementation exists only for T types that implement Clone.


27. Conditional Methods

You can use trait bounds to make methods available only when required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Container<T> {
    value: T,
}

impl<T> Container<T> {
    fn value(&self) -> &T {
        &self.value
    }
}

impl<T> Container<T>
where
    T: Clone,
{
    fn clone_value(&self) -> T {
        self.value.clone()
    }
}

Therefore:

1
2
3
4
5
6
7
Container<T>
    │
    ├── value()
    │
    └── clone_value()
          ↑
          only if T: Clone

28. Generic Trait Implementations

You can implement a trait for every type satisfying a bound:

1
2
3
4
5
6
7
8
9
10
11
12
trait Describe {
    fn describe(&self);
}

impl<T> Describe for T
where
    T: std::fmt::Debug,
{
    fn describe(&self) {
        println!("{self:?}");
    }
}

This is a blanket implementation.

It says:

Every T that implements Debug also gets this Describe implementation.


29. Blanket Implementations

The general syntax is:

1
2
3
4
5
impl<T> Trait for T
where
    T: SomeBound,
{
}

For example:

1
2
3
4
5
impl<T> MyTrait for T
where
    T: Clone,
{
}

This can be extremely powerful.

It is also subject to Rust’s orphan rules, which prevent arbitrary implementations of foreign traits for foreign types.


30. The Orphan Rule

Suppose both are defined in another crate:

1
2
Trait
Type

You cannot arbitrarily write:

1
2
impl ForeignTrait for ForeignType {
}

This restriction prevents conflicting implementations across crates.

Generally, you need to own at least one of:

1
2
trait
type

This is part of Rust’s coherence rules.


31. Generic Type Parameters Can Be Lifetimes Too

Generics and lifetimes can appear together.

For example:

1
2
3
struct Container<'a, T> {
    value: &'a T,
}

Here:

1
2
'a → lifetime parameter
T  → type parameter

You can also have multiple lifetimes:

1
2
3
4
struct Pair<'a, 'b, T> {
    first: &'a T,
    second: &'b T,
}

32. Lifetime Bounds on Generic Types

You can write:

1
2
3
4
5
fn process<'a, T>(value: &'a T)
where
    T: 'a,
{
}

Here:

1
T: 'a

means T is valid for at least lifetime 'a.

This matters when T itself could contain references.


33. Generic Functions with Lifetimes

Example:

1
2
3
fn first<'a, T>(items: &'a [T]) -> &'a T {
    &items[0]
}

Here:

1
2
'a → lifetime
T  → type

The returned reference is tied to the input slice.


34. Generic Struct + Lifetime + Trait Bound

You can combine everything:

1
2
3
4
5
6
struct Container<'a, T>
where
    T: Clone,
{
    value: &'a T,
}

You can also put the bound on the implementation instead:

1
2
3
4
5
6
7
8
9
10
11
12
struct Container<'a, T> {
    value: &'a T,
}

impl<'a, T> Container<'a, T>
where
    T: Clone,
{
    fn clone_value(&self) -> T {
        self.value.clone()
    }
}

This demonstrates how Rust’s generic system combines:

1
2
3
4
5
6
7
lifetimes
+
types
+
traits
+
bounds

35. impl Trait

Rust provides another generic-looking syntax:

1
2
3
fn create() -> impl Iterator<Item = i32> {
    0..10
}

impl Trait means:

The function returns some concrete type that implements this trait.

The caller does not need to know the concrete type.


36. impl Trait in Function Arguments

You can write:

1
2
3
fn print(value: impl std::fmt::Display) {
    println!("{value}");
}

This is roughly equivalent to:

1
2
3
fn print<T: std::fmt::Display>(value: T) {
    println!("{value}");
}

Both use static dispatch.

However, the syntax and type-system behavior are not identical in every context.


37. impl Trait in Return Position

Example:

1
2
3
fn numbers() -> impl Iterator<Item = i32> {
    0..10
}

This hides the concrete iterator type.

The caller only knows:

1
Iterator<Item = i32>

The compiler still knows the exact concrete type.

This is important:

impl Trait is not the same as dyn Trait.


38. impl Trait vs dyn Trait

Compare:

1
fn foo() -> impl Trait

with:

1
fn foo() -> Box<dyn Trait>

impl Trait generally uses static dispatch.

1
2
3
4
5
impl Trait
    ↓
compiler knows concrete type
    ↓
static dispatch

dyn Trait uses dynamic dispatch:

1
2
3
4
5
6
7
dyn Trait
    ↓
trait object
    ↓
vtable
    ↓
dynamic dispatch

39. Generic Function = Static Dispatch

Consider:

1
2
3
fn print<T: std::fmt::Display>(value: T) {
    println!("{value}");
}

When called with:

1
2
print(10);
print(3.14);

the compiler can generate specialized versions conceptually like:

1
2
print::<i32>
print::<f64>

This is called monomorphization.


40. Monomorphization

Rust generics normally use monomorphization.

Suppose:

1
2
3
fn print<T: Display>(value: T) {
    println!("{value}");
}

and:

1
2
print(10);
print(20.0);

The compiler can generate specialized machine-code versions:

1
2
print<i32>
print<f64>

Conceptually:

1
2
3
4
5
6
7
generic source
     │
     ▼
monomorphization
     │
     ├── T = i32
     └── T = f64

This is one reason generic Rust code can be both type-safe and fast.


41. Generic Code Is Not Necessarily Runtime Generic

When you write:

1
2
fn foo<T>(value: T) {
}

you might imagine one runtime function that somehow handles every possible type.

That is not generally how Rust implements it.

Rust normally specializes generic code during compilation.

Therefore the runtime usually operates on concrete machine-code versions.


42. Static Dispatch

With:

1
2
fn process<T: Trait>(value: T) {
}

the compiler knows the concrete type.

The call can therefore be statically dispatched.

Conceptually:

1
2
3
4
5
6
7
process::<TypeA>()
        ↓
known at compile time

process::<TypeB>()
        ↓
known at compile time

Advantages include:

  • optimization opportunities
  • no vtable lookup
  • no dynamic dispatch overhead
  • easier inlining

The tradeoff is potentially larger binaries due to multiple monomorphized copies.


43. Dynamic Dispatch

With:

1
2
fn process(value: &dyn Trait) {
}

the concrete type is not known by the function body.

Instead Rust uses a trait object and a vtable.

Conceptually:

1
2
3
4
5
6
7
8
9
&dyn Trait
    │
    ├── pointer to data
    │
    └── pointer to vtable
              │
              ├── method A
              ├── method B
              └── ...

The method is selected at runtime.


44. Generic vs Trait Object

Generic:

1
2
fn process<T: Trait>(value: T) {
}

Trait object:

1
2
fn process(value: &dyn Trait) {
}

Generic:

1
2
3
compile time
static dispatch
monomorphization

Trait object:

1
2
3
runtime
dynamic dispatch
vtable

45. Generic Collections

A normal Vec<T> contains one concrete type:

1
let values: Vec<i32> = vec![1, 2, 3];

You cannot do:

1
2
3
4
5
let values = vec![
    1,
    "hello",
    3.14,
];

because a Vec<T> has one T.

If you need different types, you need an abstraction such as:

1
2
3
4
5
enum Value {
    Integer(i32),
    Float(f64),
    Text(String),
}

or trait objects:

1
Vec<Box<dyn Trait>>

This distinction is important:

Generics do not mean “a collection can contain arbitrary types.”

They mean:

A generic abstraction can be instantiated with different concrete types.


46. Generic Type vs Trait Object

Compare:

1
Vec<Box<dyn Display>>

with:

1
Vec<T>

Vec<T>:

1
one concrete T

Vec<Box<dyn Display>>:

1
2
3
4
5
different concrete types
        ↓
all implement Display
        ↓
trait objects

This is a major difference between static and dynamic polymorphism.


47. Generic Associated Types

Rust also supports generic associated types (GATs).

For example:

1
2
3
4
5
6
7
trait Iterable {
    type Item<'a>
    where
        Self: 'a;

    fn item(&self) -> Self::Item<'_>;
}

Here:

1
Item<'a>

is an associated type that itself has a lifetime parameter.

GATs are particularly useful for abstractions involving borrowing.


48. Generic Associated Types with Types

Associated types can also have type parameters in supported contexts.

Conceptually:

1
2
3
trait Factory {
    type Output<T>;
}

The associated type itself becomes parameterized.

GATs are especially common when building advanced iterators, parsers, and borrowing abstractions.


49. Const Generics

Generics are not limited to types.

Rust also supports const generics.

Example:

1
2
3
struct Array<T, const N: usize> {
    data: [T; N],
}

Here:

1
2
T → type parameter
N → const parameter

Usage:

1
2
let a: Array<i32, 10>;
let b: Array<f64, 20>;

Conceptually:

1
2
Array<i32, 10>
Array<f64, 20>

50. Why Const Generics Are Useful

Before const generics, expressing an array type with a generic length was much more difficult.

Now we can write:

1
2
3
fn sum<const N: usize>(values: [i32; N]) -> i32 {
    values.iter().sum()
}

The same function works with:

1
2
sum([1, 2, 3]);
sum([1, 2, 3, 4, 5]);

The array length becomes part of the type.


51. Const Generic Syntax

The basic syntax is:

1
<const N: usize>

For example:

1
2
fn foo<const N: usize>() {
}

Multiple const parameters:

1
2
fn foo<const N: usize, const M: usize>() {
}

Combined with types:

1
2
fn foo<T, const N: usize>(value: [T; N]) {
}

52. Const Generic Expressions

You may encounter expressions involving const parameters:

1
2
3
struct Buffer<T, const N: usize> {
    data: [T; N],
}

The exact set of allowed generic const expressions depends on Rust’s current const-generic capabilities.

Simple uses such as array lengths are common and stable.


53. Generic Parameters Can Include Lifetimes, Types, and Consts

A generic declaration can contain all three:

1
2
3
struct Buffer<'a, T, const N: usize> {
    data: &'a [T; N],
}

Here:

1
2
3
'a → lifetime parameter
T  → type parameter
N  → const parameter

This is the complete conceptual family:

1
2
3
4
Generic parameters
├── lifetimes
├── types
└── const values

54. Parameter Ordering

A common generic declaration is:

1
2
struct Foo<'a, T, const N: usize> {
}

Generally, Rust syntax places:

1
2
3
4
5
lifetimes
    ↓
type parameters
    ↓
const parameters

For example:

1
2
3
4
5
fn foo<'a, T, U, const N: usize>(
    value: &'a [T; N],
    other: U,
) {
}

55. Generic Type Defaults

Generic parameters can have default types in certain declarations.

For example:

1
2
3
struct Container<T = String> {
    value: T,
}

Then:

1
2
3
let value = Container {
    value: String::from("hello"),
};

can use the default type when inference or context permits.

Defaults are more commonly encountered in library APIs and standard-library types.


56. Default Generic Type Parameters

A well-known example is:

1
2
3
4
struct Point<T = i32> {
    x: T,
    y: T,
}

Then:

1
2
3
4
Point {
    x: 10,
    y: 20,
}

can use:

1
T = i32

by default.

But:

1
2
3
4
Point::<f64> {
    x: 1.0,
    y: 2.0,
}

can explicitly choose another type.


57. Generic Type Parameters in enum

Example:

1
2
3
4
enum Either<A, B> {
    Left(A),
    Right(B),
}

Usage:

1
2
3
4
let a: Either<i32, String> = Either::Left(10);

let b: Either<i32, String> =
    Either::Right(String::from("hello"));

The two variants can carry different types.


58. Generic Functions with Multiple Type Parameters

Example:

1
2
3
fn make_pair<T, U>(first: T, second: U) -> (T, U) {
    (first, second)
}

Usage:

1
let pair = make_pair(10, "hello");

The compiler infers:

1
2
T = i32
U = &str

59. Generic Bounds Between Multiple Types

You can constrain relationships between generic types.

For example:

1
2
3
4
5
6
fn compare<T, U>(a: T, b: U)
where
    T: PartialEq<U>,
{
    println!("{}", a == b);
}

This says:

1
T can be compared with U

The generic relationship is not necessarily:

1
T == U

Instead:

1
T: PartialEq<U>

means T knows how to compare itself with U.


60. Associated Type Constraints

You can constrain an associated type:

1
2
3
4
5
6
fn sum<I>(iter: I) -> i32
where
    I: Iterator<Item = i32>,
{
    iter.sum()
}

Here:

1
I: Iterator

is the trait bound.

And:

1
Item = i32

constrains the associated type.


61. Fully Qualified Syntax

Generics become especially interesting when traits have associated functions or methods.

For example:

1
2
3
4
5
6
7
8
9
10
11
trait Animal {
    fn name() -> &'static str;
}

struct Dog;

impl Animal for Dog {
    fn name() -> &'static str {
        "Dog"
    }
}

You can call:

1
Dog::name();

or use fully qualified syntax:

1
<Type as Trait>::method()

For example:

1
<DOG as Animal>::name()

This syntax is useful when multiple traits provide methods with the same name.


62. Generic Type Syntax with Paths

Generic arguments can appear in type paths:

1
2
3
4
5
Vec<i32>
Option<String>
Result<T, E>
Box<dyn Trait>
Arc<T>

Nested generics:

1
Vec<Option<String>>

More complex:

1
HashMap<String, Vec<i32>>

63. Nested Generic Types

Generics compose naturally.

For example:

1
Vec<Option<Result<String, Error>>>

Read from the inside out:

1
2
3
4
5
6
7
String
    ↓
Result<String, Error>
    ↓
Option<Result<String, Error>>
    ↓
Vec<Option<Result<String, Error>>>

This is one of the reasons generic abstractions are powerful.


64. Generic Type Parameters and Ownership

A generic parameter can represent an owned value:

1
2
fn take<T>(value: T) {
}

When called:

1
2
3
let s = String::from("hello");

take(s);

ownership of s moves into T.

The generic parameter does not change Rust’s ownership rules.

It simply allows the function to work with different concrete types.


65. Generic References

You can borrow a generic type:

1
2
fn inspect<T>(value: &T) {
}

This means:

1
&T

for any T.

With a lifetime:

1
2
fn inspect<'a, T>(value: &'a T) {
}

Now:

1
2
'a → reference lifetime
T  → referenced type

66. Generic Mutable References

You can also write:

1
2
fn modify<T>(value: &mut T) {
}

Or explicitly:

1
2
fn modify<'a, T>(value: &'a mut T) {
}

Again:

1
2
'a → lifetime
T  → type

67. Generic Ownership vs Borrowing

Compare:

1
2
fn consume<T>(value: T) {
}

with:

1
2
fn borrow<T>(value: &T) {
}

The first takes ownership:

1
2
3
4
5
caller
  │
  └── moves value
          ↓
       function

The second borrows:

1
2
3
4
5
6
7
caller
  │
  └── owns value
       │
       └── temporary reference
                ↓
             function

Generics do not change this behavior.


68. Generic Trait Bounds and Ownership

You can combine ownership and trait bounds:

1
2
3
4
5
fn process<T>(value: T)
where
    T: Send + Sync + 'static,
{
}

This is common in concurrent Rust.

The generic parameter T is constrained by several properties.


69. Send and Sync as Generic Bounds

For example:

1
2
3
4
5
6
fn spawn<T>(value: T)
where
    T: Send + 'static,
{
    // ...
}

This expresses:

1
2
3
T can be transferred to another thread
+
T does not contain short-lived borrowed references

This is why generic APIs can encode concurrency requirements directly into the type system.


70. Sized

One important implicit bound is:

1
T: Sized

By default, generic type parameters are assumed to be Sized.

Conceptually:

1
2
fn foo<T>(value: T) {
}

means approximately:

1
2
fn foo<T: Sized>(value: T) {
}

unless you explicitly opt out.


71. ?Sized

To allow dynamically sized types, use:

1
T: ?Sized

Example:

1
2
3
fn print<T: ?Sized + std::fmt::Display>(value: &T) {
    println!("{value}");
}

?Sized means:

T does not have to be Sized.

This allows types such as:

1
2
str
dyn Trait

which are dynamically sized types.


72. Why str Needs ?Sized

str is a dynamically sized type.

You cannot normally have:

1
let x: str;

But you can have:

1
let x: &str;

A generic function taking &T can therefore be written:

1
2
fn foo<T: ?Sized>(value: &T) {
}

Now T can be:

1
2
3
Sized type
or
unsized type

73. Sized and ?Sized

Remember:

1
T

implicitly means:

1
T: Sized

while:

1
T: ?Sized

means:

1
T may or may not be Sized

This is particularly important for:

1
2
3
str
dyn Trait
[T]

74. Trait Bounds with ?Sized

You can combine them:

1
2
fn foo<T: ?Sized + Trait>(value: &T) {
}

or:

1
2
3
4
5
fn foo<T>(value: &T)
where
    T: ?Sized + Trait,
{
}

75. impl Trait and ?Sized

You will often encounter:

1
&impl Trait

or:

1
Box<dyn Trait>

The important distinction remains:

1
2
3
4
5
6
7
8
9
10
11
generic type parameter
    ↓
T

opaque return/argument type
    ↓
impl Trait

trait object
    ↓
dyn Trait

76. Generic Closures

Closures themselves have compiler-generated anonymous types.

For example:

1
let add = |a: i32, b: i32| a + b;

The closure has a unique compiler-generated type.

You usually interact with it through traits such as:

1
2
3
Fn
FnMut
FnOnce

A generic function can accept a closure:

1
2
3
4
5
6
fn apply<F>(f: F)
where
    F: Fn(i32) -> i32,
{
    println!("{}", f(10));
}

77. Generic Functions Accepting Closures

Example:

1
2
3
4
5
6
fn apply<F>(function: F, value: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    function(value)
}

Usage:

1
let result = apply(|x| x * 2, 10);

Here:

1
2
3
F
│
└── closure type

The compiler knows the concrete closure type.

Therefore this is normally statically dispatched.


78. Higher-Ranked Trait Bounds

Advanced generic code may use:

1
for<'a>

For example:

1
F: for<'a> Fn(&'a str) -> &'a str

This means:

For every lifetime 'a, F can accept a &'a str and return a &'a str.

This is useful when writing generic code over borrowing closures and functions.


79. Generic Associated Types

GAT syntax can look like:

1
2
3
4
5
6
7
trait Iterable {
    type Item<'a>
    where
        Self: 'a;

    fn item(&self) -> Self::Item<'_>;
}

This combines:

1
2
3
4
5
associated types
+
generic parameters
+
lifetimes

GATs are especially useful for APIs that return values borrowing from self.


80. Generic Trait Objects

Trait objects can contain generic associated types and associated type constraints where supported.

For example:

1
Box<dyn Iterator<Item = i32>>

Here:

1
2
3
4
5
Iterator
    ↓
associated type Item
    ↓
Item = i32

The trait object itself hides the concrete iterator type.


81. Generic Return Types

A function can return a generic type:

1
2
3
fn make<T>(value: T) -> T {
    value
}

The caller determines T.

But a function cannot normally say:

1
2
3
fn make<T>() -> T {
    // ...
}

without having some way to determine or construct the required T.

For example:

1
2
3
fn make<T: Default>() -> T {
    T::default()
}

Now the trait bound gives the function a way to create T.


82. Generic Associated Functions

A type can have a generic associated function:

1
2
3
4
5
6
7
struct Factory;

impl Factory {
    fn create<T: Default>() -> T {
        T::default()
    }
}

Usage:

1
let x: i32 = Factory::create();

or:

1
let x = Factory::create::<i32>();

83. Generic Methods with Turbofish

For:

1
2
3
4
5
impl Factory {
    fn create<T: Default>() -> T {
        T::default()
    }
}

you can write:

1
Factory::create::<i32>()

The ::<i32> specifies the generic parameter.


84. Generic Constructors

Rust does not have constructors as a special language feature.

But you can create generic constructor-like associated functions:

1
2
3
4
5
6
7
8
9
struct Container<T> {
    value: T,
}

impl<T> Container<T> {
    fn new(value: T) -> Self {
        Self { value }
    }
}

Usage:

1
2
let x = Container::new(10);
let y = Container::new(String::from("hello"));

85. Self with Generics

Inside an implementation:

1
2
3
4
5
impl<T> Container<T> {
    fn new(value: T) -> Self {
        Self { value }
    }
}

Self means:

1
Container<T>

So:

1
fn new(value: T) -> Self

is effectively:

1
fn new(value: T) -> Container<T>

86. Generic Type Aliases

You can define:

1
type Pair<T> = (T, T);

Then:

1
let point: Pair<i32> = (10, 20);

You can combine this with lifetimes:

1
type StrRef<'a> = &'a str;

Or const generics:

1
type Array<T, const N: usize> = [T; N];

87. Generic Newtype Pattern

You can wrap a type:

1
struct Wrapper<T>(T);

Usage:

1
2
let x = Wrapper(10);
let y = Wrapper(String::from("hello"));

Conceptually:

1
2
Wrapper<i32>
Wrapper<String>

Newtypes are useful for:

  • type safety
  • encapsulation
  • implementing traits
  • distinguishing otherwise identical types

88. Generic Recursive Types

You can create recursive generic structures:

1
2
3
4
enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}

Now:

1
2
List<i32>
List<String>

are possible.

The Box is necessary because recursive types need a known size.


89. Generics and Box

Consider:

1
Box<T>

Box is itself generic.

Conceptually:

1
2
3
4
5
Box<T>
  │
  ├── Box<i32>
  ├── Box<String>
  └── Box<MyType>

The same abstraction works with many types.


90. Generics and Vec

Likewise:

1
Vec<T>

is generic.

Examples:

1
2
3
4
Vec<i32>
Vec<String>
Vec<MyStruct>
Vec<Box<dyn Trait>>

The vector stores one concrete T.


91. Generic APIs in the Standard Library

Rust’s standard library is full of generics:

1
2
3
4
5
6
7
8
9
10
11
12
Option<T>
Result<T, E>
Vec<T>
Box<T>
Rc<T>
Arc<T>
Cell<T>
RefCell<T>
Mutex<T>
RwLock<T>
HashMap<K, V>
HashSet<T>

Understanding generics is therefore essential for reading Rust code.


92. Generics and RefCell

For example:

1
RefCell<T>

works for any T.

1
2
let value = RefCell::new(10);
let text = RefCell::new(String::from("hello"));

Conceptually:

1
2
RefCell<i32>
RefCell<String>

The borrowing behavior is implemented once, generically.


93. Generics and Arc

Similarly:

1
Arc<T>

can contain many types:

1
2
3
Arc<String>
Arc<Vec<u8>>
Arc<MyStruct>

The Arc implementation does not need to be rewritten for each type.


94. Generics and Smart Pointers

A useful mental model:

1
2
3
4
5
6
7
Box<T>
Rc<T>
Arc<T>
Cell<T>
RefCell<T>
Mutex<T>
RwLock<T>

all follow the pattern:

1
Wrapper<T>

where T represents the value being wrapped.

This is one of the most common uses of generics in Rust.


95. Generic Trait Bounds Define Capabilities

Consider:

1
2
3
4
5
fn save<T>(value: T)
where
    T: Serialize + Send + Sync,
{
}

The generic parameter says:

1
T can vary

The bounds say:

1
T must have these capabilities

This is a useful way to think about Rust generics:

1
2
3
4
5
6
7
Generic
    ↓
what can vary

Trait bound
    ↓
what must be possible

96. Generics and Polymorphism

Rust supports polymorphism primarily through:

Parametric polymorphism

Generics:

1
fn foo<T>(value: T) {}

Ad-hoc polymorphism

Traits:

1
2
trait Foo {
}

Dynamic polymorphism

Trait objects:

1
dyn Foo

These mechanisms solve different problems.


97. Generic Polymorphism

For:

1
2
3
fn print<T: Display>(value: T) {
    println!("{value}");
}

the same source code works with many types.

This is parametric polymorphism constrained by a trait.

For example:

1
2
3
T = i32
T = f64
T = String

as long as each type implements Display.


98. Generics vs Inheritance

Rust does not use class inheritance for generic reuse.

Instead, Rust commonly combines:

1
2
3
4
5
generics
+
traits
+
composition

For example:

1
2
fn process<T: Storage>(storage: T) {
}

The function does not care which concrete storage implementation it receives.

This provides polymorphism without requiring an inheritance hierarchy.


99. Generics and Composition

Instead of:

1
2
3
4
5
BaseClass
   ↑
DerivedClass
   ↑
MoreDerivedClass

Rust commonly uses:

1
2
3
struct Service<S> {
    storage: S,
}

with:

1
2
trait Storage {
}

Then:

1
2
3
Service<FileStorage>
Service<DatabaseStorage>
Service<MemoryStorage>

This is a powerful design pattern in Rust.


100. Generic Dependency Injection

A common pattern is:

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Storage {
    fn get(&self, key: &str);
}

struct Service<S> {
    storage: S,
}

impl<S: Storage> Service<S> {
    fn new(storage: S) -> Self {
        Self { storage }
    }
}

Now:

1
let service = Service::new(MyStorage);

The service is generic over its storage implementation.

This gives static dispatch and compile-time checking.


101. Generic State Machines

Generics can encode state in the type system.

For example:

1
2
3
4
5
6
struct Connection<State> {
    state: State,
}

struct Connected;
struct Disconnected;

Then methods can be implemented only for certain states:

1
2
3
4
5
6
7
impl Connection<Disconnected> {
    fn connect(self) -> Connection<Connected> {
        Connection {
            state: Connected,
        }
    }
}

This allows invalid state transitions to become compile-time errors.


102. Phantom Types

Sometimes a generic parameter is used only at the type level.

For example:

1
2
3
4
5
6
use std::marker::PhantomData;

struct Id<T> {
    value: u64,
    _marker: PhantomData<T>,
}

Now:

1
2
3
4
5
struct User;
struct Device;

type UserId = Id<User>;
type DeviceId = Id<Device>;

Even though both contain a u64, the types are different:

1
2
3
UserId
≠
DeviceId

This is useful for type-level safety.


103. PhantomData and Generics

PhantomData<T> tells the compiler that a type logically involves T even though it does not physically store a T.

Example:

1
2
3
4
struct Token<T> {
    id: u64,
    _marker: PhantomData<T>,
}

The generic parameter participates in type checking without requiring an actual T field.

This can also affect:

  • variance
  • ownership reasoning
  • auto traits
  • drop checking

104. Generic Associated Constants

Traits and types can also have associated constants that work with generic types.

For example:

1
2
3
trait Limits {
    const MAX: usize;
}

A generic type can implement the trait:

1
2
3
impl<T> Limits for Vec<T> {
    const MAX: usize = 1024;
}

The implementation applies to every Vec<T>.


105. Generic Drop

You can implement Drop for a generic type:

1
2
3
4
5
6
7
8
9
struct Container<T> {
    value: T,
}

impl<T> Drop for Container<T> {
    fn drop(&mut self) {
        println!("dropping");
    }
}

The implementation applies to every Container<T>.


106. Generic Trait Bounds on Associated Types

You can combine bounds:

1
2
3
4
5
6
7
fn process<I>(iter: I)
where
    I: Iterator,
    I::Item: std::fmt::Debug,
{
    // ...
}

Here:

1
2
3
I implements Iterator
+
I::Item implements Debug

This pattern is very common in generic Rust code.


107. Nested where Constraints

Complex APIs may look like:

1
2
3
4
5
6
7
fn process<T, U>(value: T, other: U)
where
    T: Clone + Send,
    U: Iterator,
    U::Item: std::fmt::Debug,
{
}

Read the bounds one at a time.

Do not try to understand the whole signature at once.


108. Generic Function with where + Lifetime

Example:

1
2
3
4
5
6
fn process<'a, T>(value: &'a T)
where
    T: 'a + std::fmt::Debug,
{
    println!("{value:?}");
}

Break it down:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'a
    ↓
lifetime

T
    ↓
generic type

T: 'a
    ↓
T is valid for 'a

T: Debug
    ↓
T supports Debug

109. Generic Function with Lifetime + Trait Object

You may encounter:

1
2
fn process<'a>(value: &'a dyn Trait) {
}

This means:

1
2
3
4
5
6
7
'a
    ↓
lifetime of the reference

dyn Trait
    ↓
trait object

Compare:

1
fn process<T: Trait>(value: &T)

with:

1
fn process(value: &dyn Trait)

The first is generic/static dispatch.

The second is dynamic dispatch.


110. Generic Function Returning Trait Objects

You can write:

1
2
3
fn create() -> Box<dyn Trait> {
    Box::new(MyType)
}

The caller does not know the concrete type.

Compare with:

1
2
3
fn create() -> impl Trait {
    MyType
}

Both hide the concrete return type, but they use different mechanisms.


111. Generic Function Returning impl Trait

Example:

1
2
3
fn numbers() -> impl Iterator<Item = i32> {
    0..10
}

The compiler knows the concrete type.

The caller only knows the trait interface.

This is an opaque type.


112. Generic Function Accepting impl Trait

Example:

1
2
3
fn process(value: impl Display) {
    println!("{value}");
}

This is approximately equivalent to:

1
2
3
fn process<T: Display>(value: T) {
    println!("{value}");
}

Use whichever syntax makes the API clearer.


113. Generic Type Parameter vs impl Trait

Compare:

1
fn foo<T: Display>(x: T) -> T

and:

1
fn foo(x: impl Display) -> impl Display

The first explicitly names T, so you can use the same type parameter elsewhere:

1
fn foo<T: Display>(x: T, y: T) -> T

With:

1
fn foo(x: impl Display, y: impl Display)

the two parameters may have different concrete types.

This is an important difference.


114. impl Trait Parameters Can Represent Different Types

For:

1
2
fn foo(x: impl Display, y: impl Display) {
}

conceptually:

1
2
3
4
5
x: T
y: U

T: Display
U: Display

So:

1
foo(10, "hello");

can work.

But:

1
2
fn foo<T: Display>(x: T, y: T) {
}

requires:

1
x and y have the same T

115. Generic Parameters Express Equality of Types

For:

1
2
3
fn pair<T>(x: T, y: T) -> (T, T) {
    (x, y)
}

both arguments must have the same type.

But:

1
2
fn pair(x: impl Display, y: impl Display) {
}

does not require the same type.

This makes generic syntax semantically important.


116. Generic Return Types Must Be Determined

Consider:

1
2
3
fn create<T>() -> T {
    // ?
}

The compiler cannot magically create an arbitrary T.

A trait bound can provide the necessary operation:

1
2
3
fn create<T: Default>() -> T {
    T::default()
}

This is a common generic design pattern.


117. Generic Bounds as Interfaces

A generic function:

1
2
fn save<T: Write>(value: T) {
}

does not care whether T is:

1
2
3
4
File
TcpStream
Vec<u8>
CustomWriter

It only requires:

1
T: Write

This is similar to programming against an interface.

But unlike inheritance-based interfaces, Rust traits are independent capabilities that types can implement.


118. Static Dispatch and Performance

Generic code:

1
2
fn process<T: Trait>(value: T) {
}

can often be optimized aggressively.

Because the compiler knows:

1
2
exact type
exact implementation

it can potentially:

  • inline calls
  • remove abstraction overhead
  • specialize code
  • optimize data layout

This is one of the major strengths of Rust’s generics.


119. The Cost of Monomorphization

Monomorphization is not free.

If a generic function is instantiated for many different types:

1
2
3
4
5
T = A
T = B
T = C
T = D
...

the binary may contain multiple specialized versions.

Potential consequences:

  • larger binaries
  • longer compilation times
  • more generated machine code

This is one reason dynamic dispatch can sometimes be useful.


120. Generics vs Dynamic Dispatch

A useful comparison:

FeatureGenericsdyn Trait
DispatchStaticDynamic
Concrete type known by compilerYesNo
MonomorphizationYesNo
VtableNoYes
Runtime dispatch costUsually noneUsually small
Binary sizeCan increaseOften smaller
Heterogeneous collectionNot directlyYes
Compile-time optimizationStrongMore limited

Neither approach is universally better.

Choose based on the API and requirements.


121. Generic Code and Zero-Cost Abstractions

Rust often aims for abstractions that compile down to code comparable to handwritten specialized code.

For example:

1
2
3
fn process<T: Trait>(value: T) {
    value.method();
}

The generic abstraction itself does not necessarily introduce runtime overhead.

The compiler can specialize it.

This is one of the reasons generics are heavily used throughout Rust’s standard library.


122. Generics and Type Safety

Generics prevent many classes of mistakes.

For example:

1
2
struct UserId(u64);
struct DeviceId(u64);

Even though both contain:

1
u64

they are different types.

Generic wrappers can extend this idea:

1
2
3
4
struct Id<T> {
    value: u64,
    _marker: PhantomData<T>,
}

Now:

1
2
Id<User>
Id<Device>

cannot accidentally be mixed.


123. Generics and API Design

When designing a generic API, ask:

What varies?

Use a generic parameter:

1
<T>

What must the type be able to do?

Use a trait bound:

1
T: Trait

Does the API need to return an abstract implementation?

Consider:

1
impl Trait

Does the API need heterogeneous values?

Consider:

1
dyn Trait

Does the type need to encode a number?

Consider:

1
const N: usize

Does the type borrow data?

Consider:

1
'a

124. Generic Syntax Cheat Sheet

One type parameter

1
<T>

Multiple type parameters

1
<T, U>

Generic function

1
2
fn foo<T>(value: T) {
}

Generic function with return type

1
2
3
fn foo<T>(value: T) -> T {
    value
}

Generic struct

1
2
3
struct Foo<T> {
    value: T,
}

Generic enum

1
2
3
enum Foo<T> {
    Value(T),
}

Generic trait

1
2
trait Foo<T> {
}

Generic implementation

1
2
impl<T> Foo<T> {
}

Generic trait implementation

1
2
impl<T> Trait for Foo<T> {
}

Trait bound

1
T: Trait

Multiple bounds

1
T: TraitA + TraitB

where

1
2
3
4
5
6
fn foo<T, U>(x: T, y: U)
where
    T: TraitA,
    U: TraitB,
{
}

Associated type constraint

1
T: Iterator<Item = i32>

impl Trait parameter

1
2
fn foo(value: impl Trait) {
}

impl Trait return

1
2
fn foo() -> impl Trait {
}

Trait object

1
dyn Trait

Boxed trait object

1
Box<dyn Trait>

Lifetime + generic type

1
2
fn foo<'a, T>(value: &'a T) {
}

Lifetime bound

1
T: 'a

Lifetime outlives

1
'a: 'b

Anonymous lifetime

1
'_

Higher-ranked trait bound

1
for<'a>

Sized

1
T: Sized

Allow unsized types

1
T: ?Sized

Const generic

1
<const N: usize>

Type + const generic

1
<T, const N: usize>

Lifetime + type + const

1
<'a, T, const N: usize>

Turbofish

1
::<T>

Multiple turbofish arguments

1
::<T, U>

125. A Complete Generic Example

Here is an example combining many of the concepts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::fmt::Debug;

struct Container<'a, T, const N: usize> {
    values: &'a [T; N],
}

impl<'a, T, const N: usize> Container<'a, T, N>
where
    T: Debug,
{
    fn print(&self) {
        println!("{:?}", self.values);
    }

    fn first(&self) -> &T {
        &self.values[0]
    }
}

This contains:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'a
    lifetime parameter

T
    type parameter

N
    const parameter

T: Debug
    trait bound

&'a [T; N]
    borrowed array

impl<'a, T, const N: usize>
    generic implementation

This is a good example of how Rust’s generic system combines multiple concepts.


126. How to Read Complex Generic Signatures

When you see a complicated Rust signature, don’t try to understand it all at once.

For example:

1
2
3
4
5
6
7
8
9
10
fn process<'a, T, U, const N: usize>(
    value: &'a [T; N],
    other: U,
) -> &'a T
where
    T: Clone + Debug,
    U: Send + 'static,
{
    &value[0]
}

Read it in layers.

Layer 1: Lifetime

1
'a

There is a lifetime parameter.

Layer 2: Types

1
2
T
U

There are two type parameters.

Layer 3: Const

1
const N: usize

There is a compile-time integer parameter.

Layer 4: Inputs

1
&'a [T; N]

A reference to an array of N values of type T.

1
U

An owned value of type U.

Layer 5: Output

1
&'a T

A reference to T tied to 'a.

Layer 6: Bounds

1
T: Clone + Debug

T must implement Clone and Debug.

1
U: Send + 'static

U must satisfy Send and 'static.

This layered approach makes complex Rust signatures much easier to understand.


127. Generics Are Compile-Time Abstraction

A useful mental model is:

1
2
3
4
5
6
7
8
9
10
11
Generic source code
       │
       ▼
Compiler
       │
       ├── T = i32
       ├── T = String
       └── T = MyType
       │
       ▼
Concrete machine code

The generic abstraction exists primarily at compile time.

This is different from dynamic dispatch, where the runtime may need to determine which implementation to call.


128. The Three Main Generic Parameter Categories

Rust’s generic parameters can be understood as three categories:

1
2
3
4
5
6
7
8
9
10
Generic parameters
│
├── Lifetime
│     'a
│
├── Type
│     T
│
└── Const
      N

For example:

1
2
3
struct Buffer<'a, T, const N: usize> {
    data: &'a [T; N],
}

contains all three.


129. Generics, Traits, and Lifetimes Work Together

These three concepts are often learned separately, but real Rust code combines them.

For example:

1
2
3
4
5
fn process<'a, T>(value: &'a T)
where
    T: Clone + Send + 'a,
{
}

Here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'a
    lifetime

T
    generic type

Clone
    capability

Send
    concurrency capability

T: 'a
    lifetime relationship

Rust’s type system allows all these constraints to be expressed together.


130. The Big Picture

Generics answer:

What type can vary?

Traits answer:

What behavior does that type provide?

Trait bounds answer:

What behavior must the generic type provide?

Lifetimes answer:

How long are borrowed references valid?

Const generics answer:

What compile-time value can vary?

impl Trait answers:

Can I hide the concrete type while exposing its capabilities?

dyn Trait answers:

Can I use runtime polymorphism through a trait object?

Monomorphization answers:

How can generic code become concrete machine code?


131. Final Mental Model

When you see:

1
fn process<T: Trait>(value: T)

think:

1
2
3
4
5
T
│
└── some concrete type
       │
       └── must implement Trait

When you see:

1
fn process<'a, T>(value: &'a T)

think:

1
2
3
4
5
6
7
'a
 │
 └── lifetime of the reference

T
 │
 └── type being referenced

When you see:

1
fn process<T, U>(x: T, y: U)

think:

1
T and U may be different types

When you see:

1
fn process<T>(x: T, y: T)

think:

1
x and y must have the same type T

When you see:

1
fn process(x: impl Trait)

think:

1
some concrete type implementing Trait

When you see:

1
fn process(x: &dyn Trait)

think:

1
2
3
trait object
+
dynamic dispatch

When you see:

1
T: ?Sized

think:

1
T may be dynamically sized

When you see:

1
<const N: usize>

think:

1
N is a compile-time value

When you see:

1
for<'a>

think:

1
for every possible lifetime 'a

And the most important idea is:

Generics allow Rust to abstract over types, lifetimes, and compile-time values while preserving strong compile-time type checking and, in the common case, enabling static dispatch and zero-cost abstractions.


This post is licensed under CC BY 4.0 by the author.