--- title: "Rust Basics - Module 5: Generics and Traits" description: "Learn Rust traits, generics, trait bounds, and static vs dynamic dispatch. Understand how Rust achieves polymorphism without runtime cost using monomorphisation" author: "Samba Siva" published: 2026-05-19T16:45:53.242Z updated: 2026-11-01T01:30:46.929Z tags: - "Technical" - "Rust" url: "https://sambasivareddy.in/blog/rust-basics-module-5-generics-and-traits" --- # Rust Basics - Module 5: Generics and Traits In the previous blog post, we discussed about the Structs, Enums, how structs help in grouping the related data under one custom data type and how enums lets us enumerate variants of same or different data types. And we end with discussing Pattern matching, a primary way of working with Enums. In this blog post, we will cover the topics, **Traits and Generics** in Rust. > **Traits** are how Rust achieves **polymorphism** - shared behaviour across different types without inheritance. **Generics** are how you write code that works over many types **without duplicating the logic.** Let's start with What problem we are trying to solve with Traits & Generics, then about Traits, Generics and bringing all together with an example. * * * ## The Problem Consider we want to find the largest elements in a slice of different data types say `i32` , `f64` etc. Without traits and generics, we need to write a separate function for each data type: ```rust fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn largest_f64(list: &[f64]) -> f64 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } ``` Here, the logic is a pure duplication of code as we wrote for two different data types and which is redundant and unnecessary. Traits and Generics helps in eliminating these problems. Before looking at the solution, here's the mental model: > Generics eliminate the duplication. Traits are constraints that implementing types must satisfy. Together, the solution would be: ```rust fn largest(list: &[T]) -> &T { ... } // works for any T that can be compared ``` - T - a generic type placeholder; can be `i32` , `f32` or even a `String` - PartialOrd - a trait constraint meaning the type must support comparison operators, or in other words, only types that implement `PartitalOrd` can be passed as a slice `&[T]` to this function. * * * ## Traits: defining shared behaviour A trait defines **a set of method signatures that a type (data type, structs, enums etc..) must implement**. Think of it as an interface. Like Java interfaces, traits define a contract - but unlike Java, Rust traits can have default implementations and can be added to existing types. ```rust trait Summary { fn summarise(&self) -> String; } ``` This says: any type that implements `Summary` must provide a `summarise` method that takes `&self` and returns a `String` . The trait does not say **how,** implementing **type decides that.** ### Example Implementing Summary trait on a type: ```rust struct Article { title: String, author: String, content: String, } struct Tweet { username: String, content: String, } impl Summary for Article { fn summarise(&self) -> String { format!("{} by {}", self.title, self.author) } } impl Summary for Tweet { fn summarise(&self) -> String { format!("{}: {}", self.username, self.content) } } ``` Now both `Article` and `Tweet` satisfy the `Summary` contract. The method call is the same regardless of which type you have: ```rust let article = Article{ title: "Rust Module-5", author: "Siva", content: "Generics & Traits", }; let tweet = Tweet{ username: "sambasiva", content: "Releasing a series on Rust", }; println!("{}", article.summarise()); // Prints "Rust Module-5 by Siva" println!("{}", tweet.summarise()); // Prints "sambasiva: Releasing a series on Rust" ``` ### Default implementation A trait can provide a **default body that types can override if they wanted to or else inherit the default behaviour**. ```rust trait Summary { fn summarise(&self) -> String { String::from("(Read more...)") // Default behaviour } } impl Summary for Article {} // Uses default summarise // Or Override it impl Summary for Tweet { fn summarise(&self) -> String { format!("{}: {}", self.username, self.content) } } fn main() { let article = Article{ title: "Rust Module-5", author: "Siva", content: "Generics & Traits", }; let tweet = Tweet{ username: "sambasiva", content: "Releasing a series on Rust", }; println!("{}", article.summarise()); // Prints "(Read more...)" println!("{}", tweet.summarise()); // Prints "sambasiva: Releasing a series on Rust" } ``` * * * ## Generics Generics **let you write a function or struct that is parameterised over a type**, resolved at compile time. First we will start with functions. ### Generic Functions ```rust fn first(list: &[T]) -> &T { &list[0] } ``` `T` is a type parameter - a placeholder. When you call `first(&[1, 2, 3])` , the compiler substitutes `T = i32` . When you call `first(&["a", "b"])` , it substitues `T = &str` . Two separate compiled functions - zero runtime cost. This is called **monomorphisation**. ### Generic Structs ```rust struct Pair { first: T, second: T, } impl Pair { fn new(first: T, second: T) -> Self { Pair { first, second } } } fn main() { let pair1 = Pair { first: 3, second: 4 } // Pass let pair2 = Pair { first: 3, second: 4.0 } // Throws error } ``` Why error for `pair2`? Because we defined only one generic parameter to the struct `Pair` i.e `T` . In case of `pair2` , `first` is `i32` and `second` is `f64` , two different data types, so the error. The fix is simple, we can pass the multiple type parameters each with different identifier. For example: ```rust struct Pair { first: T, second: K, } impl Pair { fn new(first: T, second: K) -> Self { Pair { first, second } } } fn main() { let pair1 = Pair { first: 3, second: 4 } // Pass let pair2 = Pair { first: 3, second: 4.0 } // Now Pass } ``` * * * ## Trait Bounds - constraining Generics A bare `T` can be anything - you can't call any methods on it because the compiler doesn't know what `T` supports. Traits bounds express the requirements for those types: ```rust fn largest(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { // requires PartialOrd largest = item; } } largest } ``` `T: PartialOrd` means "T must implement the `PartialOrd` trait" - i.e., `T` must support `>` , `<` , `>=` , `<=` . Without this bound, `item > largest` is a compile error because the compiler can't guarantee `T` supports **comparison.** So this function works for any type that implements `PartialOrd` - integers, floats, even `String` . A custom struct with no ordering would fail this bound. ### Multiple Bounds with "+" We can define as many trait bounds on a Generic type as we want with the help of `+` . ```rust fn print_summary(item: &T) { println!("{}", item.summarise()); } ``` In the above example, we can call this function on the types which implements both `Summary` and `std::fmt::Display` traits. `where` clause: a clear syntax for complex bounds: ```rust fn compare(t: &T, u:&U) -> String where T: Summary + Clone, U: Summary + std::fmt::Debug, { //... } ``` Both forms are equivalent - `where` is just more readable when bounds get long. * * * ## Traits as Function Parameters - impl Trait Instead of a generic type parameter, we can also use `impl Trait` syntax directly in the parameter. ```rust fn notify(item: &impl Summary) { println!("{}", item.summarise()); } ``` This says "accept any reference to any type that implements `Summary` ". It is syntactic sugar for a generic with a bound: ```rust fn notify(item: &T) { ... } // equivalent to above impl Summary ``` Use `impl Trait` for simple cases. Use explicit generics when you need to refer to `T` multiple times or return it. * * * ## Returning traits - dyn Trait Sometimes you want to return "some type that implements a trait" without specifying which. This requires a different mechanism - traits objects with `dyn` : ```rust fn make_summary(is_article: bool) -> Box { if is_article { Box::new(Article { ... }) } else { Box::new(Tweet { ... }) } } ``` `Box`is a fat pointer - a pointer to the data plus a pointer to a vtable of method implementations. The concrete type is decided at runtime, not compile time - this is slower than generics but allows more flexibility. * * * ## Key Standard Library Traits These are the traits you will encounter constantly. Knowing what they mean is essential: `Display` — defines how a type is formatted with `{}`. Implement this to make your type printable. `Debug` — defines formatting with `{:?}`. Usually derived automatically. `Clone` — provides the `.clone()` method for explicit deep copy. `Copy` — marker trait, makes assignment copy instead of move. Requires `Clone`. `PartialOrd` / `Ord` — comparison operators `<`, `>` etc. `PartialEq` / `Eq` — equality operators `==`, `!=`. `Iterator` — the trait behind every `for` loop and iterator chain. One required method: `next()`. Most of these can be **derived** automatically by the compiler for your types: ```rust #[derive(Debug, Clone, PartialEq)] struct Point { x: f64, y: f64, } let p1 = Point { x: 1.0, y: 2.0 }; let p2 = p1.clone(); println!("{:?}", p1); // requires Debug println!("{}", p1 == p2); // requires PartialEq ``` `#[derive(...)]` is a macro that auto-generates the trait implementation for you based on the struct's fields. * * * ## Complete Example ```rust use std::fmt; trait Area { fn area(&self) -> f64; fn describe(&self) -> String { format!("This shape has area {:.2}", self.area()) } } #[derive(Debug, Clone)] struct Circle { radius: f64, } #[derive(Debug, Clone)] struct Rectangle { width: f64, height: f64, } impl Area for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height } } impl fmt::Display for Circle { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Circle(r={})", self.radius) } } fn print_area(shape: &impl Area) { println!("{}", shape.describe()); } fn largest_area(shapes: &[Box]) -> f64 { shapes.iter() .map(|s| s.area()) .fold(0.0_f64, f64::max) } fn main() { let c = Circle { radius: 5.0 }; let r = Rectangle { width: 4.0, height: 6.0 }; print_area(&c); print_area(&r); let shapes: Vec> = vec![ Box::new(Circle { radius: 3.0 }), Box::new(Rectangle { width: 10.0, height: 2.0 }), Box::new(Circle { radius: 7.0 }), ]; println!("Largest area: {:.2}", largest_area(&shapes)); } ``` * * * ## Conclusion Traits and Generics together solve the duplication problem we started with - traits define _what_ a type must do, generics let you write code that works for _any_ type that qualifies. The compiler resolves generics at compile time (monomorphisation, zero cost), while `dyn Trait` defers the decision to runtime when flexibility matters more than speed. Master these two, and you hold the key to nearly every abstraction in Rust's standard library.