Traits & Generics
Welcome to Traits & Generics
Section titled “Welcome to Traits & Generics”Two of Rust’s most powerful features work together to let you write code that is both flexible and safe:
- Traits define shared behavior — a contract that any type can fulfill.
- Generics enable parametric code — one function or struct that works for many types.
Together, they let you write expressive abstractions without giving up compile-time guarantees or paying runtime costs.
Why Traits?
Section titled “Why Traits?”In many languages you share behavior through inheritance. Rust takes a different approach: you define a trait (a set of method signatures), and any type can implement it independently. This is similar to interfaces in Java or Go, but with richer capabilities like default method bodies.
trait Greet { fn hello(&self) -> String;}Any struct that implements Greet must provide a hello method. The trait is a contract; the struct fulfills that contract.
Why Generics?
Section titled “Why Generics?”Without generics you would need a separate largest_i32, largest_f64, and largest_char function — all with identical bodies but different types. Generics let you write this once using a type parameter (written inside angle brackets in code, like fn largest<T: PartialOrd>(a: T, b: T) -> T).
The T: PartialOrd part is a trait bound — it tells the compiler “this function works for any type T that supports comparison.” The compiler then generates a specialized copy for each concrete type you actually use (a process called monomorphization).
What the Compiler Says: A Generic Function With No Bound
Section titled “What the Compiler Says: A Generic Function With No Bound”Traits and generics are not two separate features you learn in sequence — a trait bound is the thing that makes a generic function’s body legal in the first place. Drop the bound and the compiler tells you exactly what capability is missing:
// @expect-error E0369fn largest<T>(a: T, b: T) -> T { if a > b { a } else { b }}
fn main() { println!("{}", largest(3, 7));}error[E0369]: binary operation `>` cannot be applied to type `T` --> src/main.rs:2:10 |2 | if a > b { a } else { b } | - ^ - T | | | T |help: consider restricting type parameter `T` with trait `PartialOrd` |1 | fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T { | ++++++++++++++++++++++T on its own means “any type at all,” and most types do not support >. The compiler’s own suggested fix is the trait bound: T: PartialOrd narrows T down to exactly the set of types where > is a legal operation. This is the recurring pattern across the whole module: a generic parameter starts out able to do almost nothing, and every trait bound you add is a deliberate trade of flexibility for capability.
Module Roadmap
Section titled “Module Roadmap”| Lesson | What you will learn |
|---|---|
| Traits | Default methods, supertraits, blanket impls, From/Into/TryFrom, operator overloading, Deref, associated types and consts |
| Generics | Generic functions, generic structs, monomorphization, where clauses, PhantomData, generic vs dyn vs enum |
| Trait Objects | dyn Trait, Box<dyn Trait>, fat pointers, vtables, dyn Any, static vs dynamic dispatch |
| Lifetimes | Lifetime annotations, elision rules, 'static, struct lifetimes, higher-ranked trait bounds |
flowchart TD
Q{What are you trying to express?}
Q -->|One shared capability many types can provide| TR[Traits lesson]
Q -->|One function or struct body for many concrete types| GEN[Generics lesson]
Q -->|A collection of mixed concrete types, chosen at runtime| TO[Trait Objects lesson]
Q -->|How long a reference stays valid| LT[Lifetimes lesson] Try It Now
Section titled “Try It Now”The snippet below shows a generic function with a trait bound, a custom trait, and a struct that implements it — all the ideas you will study in depth across this module.
fn main() { let x: i32 = 42; println!("x = {}", x);
// Generic function: works for any PartialOrd type fn largest<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
println!("largest(3, 7) = {}", largest(3, 7)); println!("largest(3.14, 2.71) = {}", largest(3.14, 2.71));
// Trait usage trait Greet { fn hello(&self) -> String; }
struct Person { name: String }
impl Greet for Person { fn hello(&self) -> String { format!("Hello, {}!", self.name) } }
let p = Person { name: String::from("Rust") }; println!("{}", p.hello());}Compiling…
Mental Model
Section titled “Mental Model”Think of a trait as answering “what can this type do,” and a generic parameter as answering “what type am I working with right now.” A trait bound (T: Trait) is the point where the two connect: it lets a generic function or struct say “I don’t care what concrete type this is, as long as it can do X.” Everything in this module — default methods, blanket impls, dyn Trait, lifetimes on generic references — is a variation on that one connection.
Trade-offs
Section titled “Trade-offs”Static dispatch (generics, trait bounds, impl Trait) costs nothing at runtime but costs compile time and binary size: every concrete type you instantiate a generic with gets its own copy of the machine code, and every constraint you add narrows what can call your function. Dynamic dispatch (dyn Trait) inverts that trade: one copy of the code, a small vtable indirection per call, and the ability to mix concrete types that were never known to each other at compile time. Neither is strictly better — the rest of this module is largely about recognizing which trade-off a given problem actually needs.
Common Misconceptions
Section titled “Common Misconceptions”- “Traits are just Rust’s version of inheritance.” They are closer to interfaces: a trait grants no data fields, no automatic field access, and no hierarchy between types. Two types that implement the same trait are not related to each other in any way beyond that shared contract — composition, not inheritance.
- “Generics and trait bounds are two different topics.” A bare generic parameter
Tcan do almost nothing — no comparison, no printing, no arithmetic — until a trait bound grants it a specific capability. Learning generics is learning which trait bounds unlock which operations. - “
dyn Traitis the ‘advanced’ or ‘better’ way to use traits, and generics are the beginner version.” Neither is more advanced — they solve different problems. Reaching fordyn Traitby default costs a vtable indirection and heap allocation you don’t need when the type is actually known at compile time.
Reading AI-generated code
Section titled “Reading AI-generated code”Generated code frequently adds a Clone bound to a generic function “just in case,” then calls .clone() inside a loop on a value that only ever needed to be read by reference — the bound compiles, the clone runs, and nothing looks obviously wrong at a glance.
🐛 Spot the bug
fn sum_lengths<T: Clone + AsRef<str>>(items: &[T]) -> usize { let mut total = 0; for item in items { let owned = item.clone(); // cloned every iteration for no reason total += owned.as_ref().len(); } total}
fn main() { let words = vec![String::from("hello"), String::from("world")]; println!("{}", sum_lengths(&words));}Show the answer
This compiles and prints the correct answer (10), which is exactly why it survives review — the bug is a wasted allocation, not a wrong result. .clone() inside the loop duplicates every String just to call .as_ref().len(), a method that only ever needed a borrow. The Clone bound is not doing any useful work here either: dropping it down to fn sum_lengths<T: AsRef<str>>(items: &[T]) -> usize and calling item.as_ref().len() directly removes the allocation and the unnecessary bound in one edit — the smallest bound that grants the capability you actually use is the correct one, not the broadest one that happens to compile.