DEV Community

Sureni Dhanasekara
Sureni Dhanasekara

Posted on

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}");
}
Enter fullscreen mode Exit fullscreen mode

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<T>(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}");
}
Enter fullscreen mode Exit fullscreen mode

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<T>(x: T) -> T {
    x
}
Enter fullscreen mode Exit fullscreen mode
  • Structs
struct Point<T> {
    x: T,
    y: T,
}
Enter fullscreen mode Exit fullscreen mode
  • Enums
enum Option<T> {
    Some(T),
    None,
}
Enter fullscreen mode Exit fullscreen mode
  • Traits & Implementations
impl<T: std::fmt::Debug> Point<T> {
    fn show(&self) {
        println!("{:?}, {:?}", self.x, self.y);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Methods With Multiple Generics
struct Pair<T, U> {
    first: T,
    second: U,
}
Enter fullscreen mode Exit fullscreen mode

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<T>(x: T) -> T

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

β†’ Compiler generates:
- fn identity_i32(x: i32) -> i32
- fn identity_str(x: &str) -> &str
Enter fullscreen mode Exit fullscreen mode

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()
    }
}

Enter fullscreen mode Exit fullscreen mode

Default Implementations

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

Using where Clauses for Readability
Instead of:

fn process<T: Greet, U: Farewell>(t: T, u: U)
Enter fullscreen mode Exit fullscreen mode

Use:

fn process<T, U>(t: T, u: U)
where
    T: Greet,
    U: Farewell,
{
    println!("{} {}", t.greet(), u.goodbye());
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
pradeep-sanjaya profile image
Pradeep Sanjaya

These types also implement Copy trait:

  • Function pointers
  • Immutable references (&T)
  • Fixed-size arrays of Copy types, like [i32; 5]