Rust 37 🦀 Generic Data Types in Functions, Structs, and Enums
Generics are Rust’s mechanism for writing code once and using it with many types. Without generics, a function that finds the largest element in a list would need a separate version for i32, f64, char, and every other type. With generics, one function works for all of them, and the compiler generates the specialized code for each concrete type it is used with. The syntax is angle brackets: fn largest<T>(list: &[T]) -> &T. The T is a type parameter, a placeholder that is filled in when the function is called.
The key property of Rust generics is monomorphization. The compiler does not generate one generic function that works on all types at runtime. It generates a separate, specialized copy of the function for each concrete type it is used with. A largest::<i32> and a largest::<f64> become two distinct functions in the compiled binary. This means generics have zero runtime cost: the generated code is exactly what you would have written by hand for each type. The trade-off is compilation time and binary size, since each instantiation produces its own copy of the code.
Generics apply to functions, structs, enums, and methods. A Point<T> struct can hold x and y of any type. A Result<T, E> enum can carry a success value of one type and an error of another. A method can be generic over a type that is not the struct’s type parameter, or it can be implemented only for specific concrete types. The combination of generics and traits is what makes Rust’s abstraction system work: a generic function can require that its type parameter implements a trait, which guarantees the operations the function needs are available.
This chapter covers three areas. First, why generics exist — the duplication problem, the difference between generics and dynamic dispatch, and the monomorphization model. Second, how to define generic functions, structs, and enums — the syntax, the type parameter declarations, and the rules for using type parameters in fields and return types. Third, how to implement methods on generic types — impl<T> Point<T>, impl Point<f64>, and the difference between generic methods and generic impl blocks. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing monomorphization and the generic type parameter flow.
Key point: Generics let you write code that works with many types without duplicating it. The compiler monomorphizes generic code, generating a specialized copy for each concrete type used, so generics have zero runtime cost. Type parameters are declared in angle brackets and can be used in functions, structs, enums, and methods .
Why generics exist
The duplication problem. Without generics, a function that operates on a list of i32 must be rewritten for f64, String, and every other type. The logic is identical; only the type changes. Copying the function for each type means the logic exists in multiple places, and a bug fix must be applied to every copy. Generics eliminate the duplication. One function, parameterized by type, serves all types. The logic exists once, and the compiler generates the specialized versions .
The type-safety problem. A dynamically typed language can write a single function that works on any type, but the type checking happens at runtime. If the function calls a method that the type does not have, the error occurs when the code runs, not when it is compiled. Rust generics are checked at compile time. The compiler knows the concrete type at the call site, and it verifies that the operations the function performs are valid for that type. A generic function that calls > on its type parameter must declare that the type parameter implements PartialOrd. If a caller passes a type that does not, the code does not compile .
The monomorphization problem. Generics could be implemented in two ways: monomorphization (generate specialized code for each type) or type erasure (generate one generic function and use runtime type information). Rust chooses monomorphization. The compiler substitutes the concrete type into the generic code and generates a specialized version. This means a generic function call has no runtime dispatch overhead — it is a direct call to the specialized function. The trade-off is that each instantiation produces its own copy of the code, which increases binary size and compilation time. For most programs, the benefit of zero-cost abstraction outweighs the cost .
The abstraction problem. Generics alone are not enough. A generic function that tries to compare two values of type T cannot compile unless the compiler knows that T supports comparison. Traits are the mechanism that provides this guarantee. A trait bound like T: PartialOrd tells the compiler that any type used with this function must implement PartialOrd, which provides the > operator. The combination of generics and traits is what makes Rust’s abstractions both flexible and type-safe. The generic function is written once, works with many types, and the trait bound ensures that the operations it performs are valid for every type it accepts .
The zero-cost problem. The promise of zero-cost abstraction is that high-level code compiles to the same machine code as hand-written low-level code. Monomorphization delivers this promise for generics. A generic largest::<i32> compiles to the same machine code as a hand-written largest_i32. There is no vtable lookup, no boxing, no runtime type check. The abstraction is free at runtime. This is what distinguishes Rust generics from generics in languages that use type erasure, where every generic operation goes through a dispatch layer .
The trade-off. Monomorphization increases compile time and binary size. A generic function used with ten types produces ten copies of the code. In a large codebase with deeply nested generics, this can lead to code bloat and long compile times. The alternative, dynamic dispatch with trait objects (dyn Trait), produces a single copy of the code but adds a vtable lookup at each call. The choice between generics and trait objects is the choice between compile-time specialization and runtime flexibility. Generics are appropriate when the set of types is known at compile time and performance matters; trait objects are appropriate when the set of types is open or when binary size is a concern.
a. Generic functions
A generic function declares one or more type parameters in angle brackets after the function name. The type parameters can be used in the parameter list, the return type, and the function body .
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
The <T: PartialOrd> declares a type parameter T with a trait bound. The bound says that T must implement PartialOrd, which provides the > operator. Without the bound, the comparison item > largest would not compile, because the compiler cannot assume that every type supports comparison .
The function can be called with any type that implements PartialOrd:
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("{}", result); // 100
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("{}", result); // 'y'
The compiler generates two specializations: largest::<i32> and largest::<char>. Each is a separate function in the binary, compiled for its specific type.
Trait bounds can be written inline (<T: PartialOrd>) or in a where clause. The where clause is preferred when the bounds are complex or when there are many of them:
fn largest<T>(list: &[T]) -> &T
where
T: PartialOrd,
{
// ...
}
Multiple type parameters are separated by commas: fn pair<T, U>(a: T, b: U) -> (T, U). Each parameter can have its own bounds.
b. Generic structs
A struct can be generic over one or more types. The type parameters are declared after the struct name and used in the field types .
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
The Point<T> struct has two fields of type T. When Point { x: 5, y: 10 } is created, T is inferred as i32. When Point { x: 1.0, y: 4.0 } is created, T is inferred as f64. The two points are different types: Point<i32> and Point<f64> .
A struct with multiple type parameters can have fields of different types:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
The third point has T = i32 and U = f64. The struct definition allows any combination, and the compiler infers the concrete types at each use site.
The type parameters of a struct can be constrained with trait bounds, but the bounds are usually placed on the impl block rather than the struct definition. This allows the struct to be instantiated with any type, while the methods are only available when the type satisfies the bounds.
c. Generic enums
Enums can be generic in the same way as structs. The standard library’s Option<T> and Result<T, E> are the canonical examples .
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
The Option<T> enum is generic over one type. The Some variant holds a T, and the None variant holds nothing. The Result<T, E> enum is generic over two types: the success value and the error value. These enums are so fundamental that they are in the prelude, but they are ordinary generic enums defined with the same syntax .
A custom generic enum follows the same pattern:
enum Either<L, R> {
Left(L),
Right(R),
}
The Either enum holds either a value of type L or a value of type R. It is useful for functions that can return one of two different types, and it is the basis for many combinator patterns.
d. Generic methods and impl blocks
Methods are defined in impl blocks. An impl block can be generic over the struct’s type parameters, or it can be specific to a concrete type .
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
The first impl<T> Point<T> block defines a method x that is available for any Point<T>. The second impl Point<f64> block defines a method distance_from_origin that is only available for Point<f64>. The method uses powi and sqrt, which are specific to floating-point types, so it cannot be defined for all T. The two impl blocks coexist: Point<f64> has both methods, and Point<i32> has only x .
A method can introduce its own type parameters that are independent of the struct’s:
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
The mixup method takes self (a Point<T, U>) and other (a Point<V, W>) and returns a Point<T, W>. The type parameters V and W are introduced by the method and are independent of the struct’s T and U. This allows combining two points with different types into a new point with a mix of the type parameters .
Complete Example Session
// ============================================
// PART 1: A GENERIC FUNCTION
// ============================================
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("{}", largest(&numbers)); // 100
let chars = vec!['y', 'm', 'a', 'q'];
println!("{}", largest(&chars)); // 'y'
}
// ============================================
// PART 2: WHERE CLAUSE
// ============================================
fn largest_where<T>(list: &[T]) -> &T
where
T: PartialOrd,
{
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
// ============================================
// PART 3: A GENERIC STRUCT
// ============================================
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 }; // Point<i32>
let float = Point { x: 1.0, y: 4.0 }; // Point<f64>
}
// ============================================
// PART 4: MULTIPLE TYPE PARAMETERS
// ============================================
struct Pair<T, U> {
first: T,
second: U,
}
fn main() {
let p1 = Pair { first: 1, second: "one" };
// Pair<i32, &str>
let p2 = Pair { first: 1.5, second: true };
// Pair<f64, bool>
}
// ============================================
// PART 5: A GENERIC ENUM
// ============================================
enum Either<L, R> {
Left(L),
Right(R),
}
fn divide(a: f64, b: f64) -> Either<f64, String> {
if b == 0.0 {
Either::Right("division by zero".to_string())
} else {
Either::Left(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Either::Left(result) => println!("result: {}", result),
Either::Right(error) => println!("error: {}", error),
}
}
// ============================================
// PART 6: GENERIC IMPL BLOCK
// ============================================
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("x: {}", p.x()); // x: 5
}
// ============================================
// PART 7: CONCRETE IMPL BLOCK
// ============================================
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("distance: {}", p.distance_from_origin()); // 5
// let q = Point { x: 3, y: 4 };
// q.distance_from_origin();
// ^ error: method not found for Point<i32>
}
// ============================================
// PART 8: METHOD WITH ITS OWN TYPE PARAMETERS
// ============================================
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
// p3.x is i32, p3.y is char
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
// ============================================
// PART 9: TRAIT BOUNDS ON STRUCTS
// ============================================
struct Wrapper<T: std::fmt::Display> {
value: T,
}
impl<T: std::fmt::Display> Wrapper<T> {
fn print(&self) {
println!("{}", self.value);
}
}
fn main() {
let w = Wrapper { value: 42 };
w.print(); // 42
let w2 = Wrapper { value: "hello" };
w2.print(); // hello
}
// ============================================
// PART 10: MONOMORPHIZATION IN ACTION
// ============================================
// The compiler generates specialized versions
// of generic functions for each concrete type.
fn identity<T>(value: T) -> T {
value
}
fn main() {
let a = identity(42); // identity::<i32>
let b = identity("hello"); // identity::<&str>
let c = identity(3.14); // identity::<f64>
// Three separate functions in the binary,
// each specialized for its type.
}
The ten parts show generic functions, where clauses, generic structs, multiple type parameters, generic enums, generic impl blocks, concrete impl blocks, methods with their own type parameters, trait bounds on structs, and monomorphization.
Quick Reference
Generic Syntax
| Construct | Syntax |
|---|---|
| Generic function | fn name<T>(param: T) -> T |
| Generic struct | struct Name<T> { field: T } |
| Generic enum | enum Name<T> { Variant(T) } |
| Generic impl | impl<T> Name<T> { ... } |
| Concrete impl | impl Name<f64> { ... } |
| Method type parameter | fn method<U>(...) |
| Trait bound inline | <T: Trait> |
| Trait bound where | where T: Trait |
Multiple Type Parameters
| Syntax | Meaning |
|---|---|
<T, U> | Two independent type parameters |
Point<T, U> | Struct with two type parameters |
Result<T, E> | Enum with two type parameters |
fn pair<T, U>(a: T, b: U) | Function with two type parameters |
Impl Block Rules
| Block | Availability |
|---|---|
impl<T> Point<T> | Methods for any Point<T> |
impl Point<f64> | Methods only for Point<f64> |
impl<T, U> Point<T, U> | Methods for any Point<T, U> |
impl<T: Display> Point<T> | Methods when T: Display |
Monomorphization
| Generic call | Generated |
|---|---|
largest(&[1, 2, 3]) | largest::<i32> |
largest(&['a', 'b']) | largest::<char> |
largest(&[1.0, 2.0]) | largest::<f64> |
Best Practices
✅ Do This:
// Use where clauses for complex bounds
fn process<T>(item: T) -> T where T: Display + Clone { ... } // ✅
// Place bounds on impl blocks, not structs
impl<T: Display> Wrapper<T> { fn print(&self) { ... } } // ✅
// Use concrete impl for type-specific methods
impl Point<f64> { fn distance(&self) -> f64 { ... } } // ✅
// Use descriptive type parameter names
fn map<T, U>(item: T, f: impl Fn(T) -> U) -> U { ... } // ✅
// Prefer generics over trait objects when types are known
fn largest<T: PartialOrd>(list: &[T]) -> &T { ... } // ✅
❌ Don’t Do This:
// Don't put unnecessary bounds on struct definitions
struct Wrapper<T: Display> { value: T } // use impl instead // ❌
// Don't use single-letter names for complex parameters
fn process<A, B, C, D>(...) // hard to read // ❌
// Don't duplicate code for each type
fn largest_i32(...) {} fn largest_f64(...) {} // ❌
// Don't over-constrain the struct
struct Point<T: Add> { x: T, y: T } // bound on struct, not impl // ❌
// Don't use generics when a concrete type suffices
fn add<T: Add<Output = T>>(a: T, b: T) -> T { a + b } // fine, but... // ✅
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Missing trait bound | Comparison or method call fails | Add T: Trait bound |
| Struct bound too restrictive | Bound on struct, not impl | Move bound to impl |
| Method not found | Concrete impl not matching type | Check impl block type |
| Type inference fails | Ambiguous generic parameter | Annotate the type |
T not in scope in impl | Missing <T> on impl | Add impl<T> Name<T> |
| Monomorphization bloat | Too many generic instantiations | Consider dyn Trait |
where clause placement | Wrong position | After return type, before body |
Real-World Examples
1. Generic Function
fn identity<T>(value: T) -> T { value }
2. Generic Struct
struct Wrapper<T> { value: T }
3. Generic Enum
enum Either<L, R> { Left(L), Right(R) }
4. Generic Impl
impl<T> Wrapper<T> { fn get(&self) -> &T { &self.value } }
5. Concrete Impl
impl Wrapper<f64> { fn sqrt(&self) -> f64 { self.value.sqrt() } }
6. Where Clause
fn print_all<T>(items: &[T]) where T: Display { ... }
7. Multiple Bounds
fn process<T: Display + Clone>(item: T) -> T { ... }
8. Method Type Parameter
impl<T> Point<T> { fn convert<U>(self, f: impl Fn(T) -> U) -> Point<U> { ... } }
9. Trait Bound on Struct
struct Displayable<T: Display> { value: T }
10. Monomorphization
identity(42); identity("hello"); // two specializations
Visual
Monomorphization
┌──────────────────────────────────────────────────────────────┐
│ MONOMORPHIZATION │
│ │
│ Source: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ fn identity<T>(value: T) -> T { value } │ │
│ │ │ │
│ │ let a = identity(42); │ │
│ │ let b = identity("hello"); │ │
│ │ let c = identity(3.14); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Generated: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ fn identity_i32(value: i32) -> i32 { value } │ │
│ │ fn identity_str(value: &str) -> &str { value } │ │
│ │ fn identity_f64(value: f64) -> f64 { value } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ One generic function, three specialized functions. │
│ Zero runtime cost. More code in the binary. │
│ │
└──────────────────────────────────────────────────────────────┘
Generic Struct Instantiations
┌──────────────────────────────────────────────────────────────┐
│ GENERIC STRUCT INSTANTIATIONS │
│ │
│ struct Point<T> { x: T, y: T } │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ let p1 = Point { x: 5, y: 10 }; │ │
│ │ → Point<i32> │ │
│ │ │ │
│ │ let p2 = Point { x: 1.0, y: 4.0 }; │ │
│ │ → Point<f64> │ │
│ │ │ │
│ │ let p3 = Point { x: "a", y: "b" }; │ │
│ │ → Point<&str> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Each instantiation is a distinct type. The methods │
│ available depend on the type parameter. │
│ │
└──────────────────────────────────────────────────────────────┘
Impl Block Specialization
┌──────────────────────────────────────────────────────────────┐
│ IMPL BLOCK SPECIALIZATION │
│ │
│ impl<T> Point<T> { │
│ fn x(&self) -> &T { &self.x } │
│ } │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Available for ALL Point<T>: │ │
│ │ Point<i32> → x() │ │
│ │ Point<f64> → x() │ │
│ │ Point<&str> → x() │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ impl Point<f64> { │
│ fn distance_from_origin(&self) -> f64 { ... } │
│ } │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Available only for Point<f64>: │ │
│ │ Point<f64> → distance_from_origin() │ │
│ │ Point<i32> → ERROR: method not found │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Generic impls provide shared methods. Concrete impls │
│ provide type-specific methods. │
│ │
└──────────────────────────────────────────────────────────────┘
Type Parameter Flow
┌──────────────────────────────────────────────────────────────┐
│ TYPE PARAMETER FLOW │
│ │
│ fn largest<T: PartialOrd>(list: &[T]) -> &T │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ T is declared in <T: PartialOrd> │ │
│ │ T is used in &[T] (parameter) │ │
│ │ T is used in -> &T (return) │ │
│ │ │ │
│ │ The bound PartialOrd guarantees: │ │
│ │ item > largest (comparison operator) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ At the call site: │
│ largest(&[1, 2, 3]) → T = i32 │
│ largest(&['a', 'b']) → T = char │
│ │
│ The compiler substitutes the concrete type into T and │
│ verifies that it implements PartialOrd. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Generic function | fn name<T>(param: T) -> T |
| Generic struct | struct Name<T> { field: T } |
| Generic enum | enum Name<T> { Variant(T) } |
| Generic impl | impl<T> Name<T> { ... } |
| Concrete impl | impl Name<f64> { ... } |
| Trait bound | <T: Trait> or where T: Trait |
| Multiple parameters | <T, U> |
| Method type parameter | fn method<U>(...) |
| Monomorphization | Specialized code per concrete type |
| Zero-cost | No runtime dispatch for generics |
Key takeaways:
- Generics eliminate duplication without sacrificing type safety. One function, struct, or enum works with many types, and the compiler verifies that the operations used are valid for each concrete type .
- Monomorphization generates specialized code. The compiler produces a separate copy of the generic code for each concrete type it is used with. There is no runtime dispatch, and generics have zero runtime cost. The trade-off is compile time and binary size .
- Trait bounds constrain the type parameter. A generic function that calls
>must declareT: PartialOrd. The bound guarantees that the operation is available for every type the function accepts . - The
whereclause is for complex bounds. It is equivalent to inline bounds but more readable when there are many bounds or when the bounds are long . - Generic structs can have multiple type parameters.
Point<T, U>allows the fields to have different types, and each combination of concrete types produces a distinct struct type . - Generic enums are the basis for
OptionandResult. The same syntax used for custom enums defines the standard library’s most important types . impl<T>provides methods for all instantiations;impl Concreteprovides methods for one. The two forms coexist, and a type can have both generic and type-specific methods .- A method can introduce its own type parameters.
fn mixup<V, W>onimpl<T, U> Point<T, U>allows combining points with different type parameters, returning a new point with a mix of types.
Remember: Generics are Rust’s mechanism for writing code once and using it with many types. The type parameter is a placeholder, and the trait bound is a guarantee about what the placeholder can do. The compiler monomorphizes the generic code, generating a specialized version for each concrete type, so there is no runtime cost. Generics apply to functions, structs, enums, and methods, and the impl block determines which methods are available for which instantiations. The combination of generics and traits is what makes Rust’s abstractions both flexible and free.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!