Rust Generics and Traits

Generics allows you to write versatile and reusable codes which are in multiple types without duplications. Simply Instead of writing separate functions for i32, f64, and String, you write one generic function that works with all of them. They are denoted by angle brackets () Why Use Generics Code reusability: Write once, use for any type. Type safety: Compiler checks for correct types at compile time. Performance: No runtime overhead Problem Example: Finding the Largest Item in a List Without generics, your code looks like this: fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {result}"); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {result}"); } In here we can see it duplicated code for largest_i32 and largest_char. We can use generics in this code like as follows: Rewriting with Generics fn largest(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {result}"); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {result}"); } T: PartialOrd is a trait bound that ensures the type supports comparison using >. This function now works for i32, char, and any type implementing PartialOrd. Where Are Generic Types Used? Functions fn identity(x: T) -> T { x } Structs struct Point { x: T, y: T, } Enums enum Option { Some(T), None, } Traits & Implementations impl Point { fn show(&self) { println!("{:?}, {:?}", self.x, self.y); } } Methods With Multiple Generics struct Pair { first: T, second: U, } Monomorphization Rust uses a process called monomorphization during compilation: For each concrete type used with a generic, it generates a version of the function or struct for that type. This means no runtime overhead, unlike Java or C++ templates. Generic Function: fn identity(x: T) -> T Used with: - identity(10) - identity("hello") → Compiler generates: - fn identity_i32(x: i32) -> i32 - fn identity_str(x: &str) -> &str What Are Traits? In Rust, traits are like interfaces in other languages. They define behavior that types can implement. Traits help constrain generics to ensure type safety and functionality. Defining and Implementing Traits trait Greet { fn greet(&self) -> String; } struct Person; impl Greet for Person { fn greet(&self) -> String { "Hello!".to_string() } } Default Implementations trait Farewell { fn goodbye(&self) -> String { "Goodbye!".to_string() } } Using where Clauses for Readability Instead of: fn process(t: T, u: U) Use: fn process(t: T, u: U) where T: Greet, U: Farewell, { println!("{} {}", t.greet(), u.goodbye()); } Common Mistakes with Generics Forgetting Trait Bounds If you try to compare or print generic types without proper trait bounds like PartialOrd, Debug, or Display, you'll get compiler errors. Overcomplicating Generics Not everything needs to be generic. Use when there's clear duplication or type flexibility needed. Type Inference Limits Sometimes the compiler can't infer the type, especially in complex chains. You might need to explicitly annotate types.

Apr 20, 2025 - 13:31
 0
Rust Generics and Traits

Generics allows you to write versatile and reusable codes which are in multiple types without duplications. Simply Instead of writing separate functions for i32, f64, and String, you write one generic function that works with all of them. They are denoted by angle brackets (<>)

Why Use Generics

  • Code reusability: Write once, use for any type.

  • Type safety: Compiler checks for correct types at compile time.

  • Performance: No runtime overhead

Problem Example: Finding the Largest Item in a List

Without generics, your code looks like this:

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {result}");
}

In here we can see it duplicated code for largest_i32 and largest_char. We can use generics in this code like as follows:

Rewriting with Generics

fn largest(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {result}");
}

T: PartialOrd is a trait bound that ensures the type supports comparison using >.

This function now works for i32, char, and any type implementing PartialOrd.

Where Are Generic Types Used?

  • Functions
fn identity(x: T) -> T {
    x
}
  • Structs
struct Point {
    x: T,
    y: T,
}
  • Enums
enum Option {
    Some(T),
    None,
}
  • Traits & Implementations
impl Point {
    fn show(&self) {
        println!("{:?}, {:?}", self.x, self.y);
    }
}
  • Methods With Multiple Generics
struct Pair {
    first: T,
    second: U,
}

Monomorphization
Rust uses a process called monomorphization during compilation:

  • For each concrete type used with a generic, it generates a version of the function or struct for that type.

  • This means no runtime overhead, unlike Java or C++ templates.

Generic Function:
fn identity(x: T) -> T

Used with:
- identity(10)
- identity("hello")

→ Compiler generates:
- fn identity_i32(x: i32) -> i32
- fn identity_str(x: &str) -> &str

What Are Traits?
In Rust, traits are like interfaces in other languages. They define behavior that types can implement. Traits help constrain generics to ensure type safety and functionality.

Defining and Implementing Traits

trait Greet {
    fn greet(&self) -> String;
}

struct Person;

impl Greet for Person {
    fn greet(&self) -> String {
        "Hello!".to_string()
    }
}

Default Implementations

trait Farewell {
    fn goodbye(&self) -> String {
        "Goodbye!".to_string()
    }
}

Using where Clauses for Readability
Instead of:

fn process(t: T, u: U)

Use:

fn process(t: T, u: U)
where
    T: Greet,
    U: Farewell,
{
    println!("{} {}", t.greet(), u.goodbye());
}

Common Mistakes with Generics

  • Forgetting Trait Bounds
    If you try to compare or print generic types without proper trait bounds like PartialOrd, Debug, or Display, you'll get compiler errors.

  • Overcomplicating Generics
    Not everything needs to be generic. Use when there's clear duplication or type flexibility needed.

  • Type Inference Limits
    Sometimes the compiler can't infer the type, especially in complex chains. You might need to explicitly annotate types.