Traits & Generics
Traits describe shared behavior across unrelated types, and generics let one function work over all of them without sacrificing speed.
A trait is Rust's way of describing behavior that different types can share, without those types needing to be related by inheritance. If you've used interfaces in Java or TypeScript, or protocols in Swift, the shape will feel familiar: a trait declares a set of method signatures, and any type can opt in by implementing them. What makes Rust's traits more than a syntactic nicety is that the compiler uses them everywhere — operator overloading, formatting with {} and {:?}, iteration, equality, even thread safety are all just traits (Add, Display, Debug, Iterator, PartialEq, Send) that ordinary types implement. Once you can define and implement your own traits, you're using the same mechanism the standard library runs on, not a separate beginner's feature.
Defining a trait looks like declaring an interface: trait Summary { fn summarize(&self) -> String; } lists a method signature with no body. A type opts in with impl Summary for Article { fn summarize(&self) -> String { ... } }, and it must supply every method the trait declares that doesn't already have a default implementation — the compiler will refuse to compile a type that only partially implements a trait. Notice that the trait and the implementation are two separate blocks: this separation is what lets you implement traits from the standard library (like Display) for your own types, and — subject to Rust's orphan rule — implement your own traits for types you didn't write, such as Vec<T>.
Traits can also carry default method bodies, which is where they earn their keep over a plain interface. If summarize has a sensible fallback implementation written in terms of another method the trait requires (like author), every implementing type gets that behavior for free and only needs to override it when it actually wants something different. This flips the usual boilerplate: instead of every type re-implementing shared logic, only the types that need custom behavior write any code at all.
Generics are where traits start doing real work. A function like fn largest<T: PartialOrd>(list: &[T]) -> &T is generic over any type T, but it constrains T with a trait bound: T must implement PartialOrd, because the function's body compares elements with >. Without that bound, the compiler would have to accept a T that might not support comparison at all, and it refuses to gamble — it checks the bound at the definition site, not at every call site. This is also why Rust's generics don't cost you anything at runtime: the compiler generates a separate, fully concrete version of largest for every type you actually call it with (a process called monomorphization), so the compiled code is exactly as fast as if you'd hand-written largest_i32 and largest_char separately.
When a function only needs to accept 'something that implements a trait' rather than work with an explicit generic, impl Trait is shorthand for exactly that. fn notify(item: &impl Summary) reads naturally and is equivalent to writing fn notify<T: Summary>(item: &T) — the compiler still monomorphizes it, so it's just syntax sugar, not a different dispatch mechanism. impl Trait also works in return position — fn create_tweet() -> impl Summary lets a function return some concrete type without revealing which one, which is invaluable when that type is unnameable, like the type of a closure or an iterator adaptor chain.
There's a real difference in return position, though: impl Trait still commits the function to returning exactly one concrete type, chosen at compile time — you can't have one branch return a Tweet and another return an Article from the same impl Summary-returning function. If you genuinely need to hand back different concrete types behind one interface, or store a mix of types in the same collection, you need dynamic dispatch instead.
That's what dyn Trait is for. A &dyn Summary or Box<dyn Summary> erases the concrete type behind a vtable — a table of function pointers built at runtime — so a Vec<Box<dyn Summary>> can genuinely hold a Tweet and an Article side by side, and each call to .summarize() is resolved at runtime rather than compiled to a direct call. That flexibility isn't free: it costs an indirect call and a heap allocation (via Box) that static dispatch avoids entirely. For now, treat dyn Trait as the escape hatch for heterogeneous collections and think of the choice as static-by-default, dynamic-when-you-need-it — later lessons on smart pointers will use it constantly.
trait Summary {fn author(&self) -> String;// A default method: any type gets this for free unless it overrides it.fn summarize(&self) -> String {format!("(Read more from {}...)", self.author())}}struct Article {headline: String,author_name: String,}impl Summary for Article {fn author(&self) -> String {self.author_name.clone()}// Article overrides the default because it wants a different format.fn summarize(&self) -> String {format!("{}, by {}", self.headline, self.author_name)}}struct Tweet {username: String,}impl Summary for Tweet {fn author(&self) -> String {format!("@{}", self.username)}// No override here: Tweet relies on the default summarize().}fn main() {let article = Article {headline: String::from("Rust 2.0 Announced"),author_name: String::from("Jane Doe"),};let tweet = Tweet {username: String::from("ferris"),};println!("{}", article.summarize());println!("{}", tweet.summarize());}
Defining a trait with one required method and one default method, then implementing it for two different types — Tweet gets the default summarize() for free, Article overrides it.
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];let result = largest(&numbers);println!("The largest number is {}", result);let chars = vec!['y', 'm', 'a', 'q'];let result = largest(&chars);println!("The largest char is {}", result);}
A generic function bounded by PartialOrd: the bound is required because the function body compares elements with >, and the compiler generates a separate concrete version for each type it's called with.
trait Summary {fn summarize(&self) -> String;}struct Tweet {username: String,content: String,}impl Summary for Tweet {fn summarize(&self) -> String {format!("@{}: {}", self.username, self.content)}}// `impl Trait` in argument position: sugar for a generic bound.fn notify(item: &impl Summary) {println!("Breaking news! {}", item.summarize());}// `impl Trait` in return position: callers get a concrete type back// without needing to know which one it is.fn create_tweet() -> impl Summary {Tweet {username: String::from("rustlang"),content: String::from("Rust 1.80 is out!"),}}// `dyn Trait` behind a reference: works uniformly with several concrete// types through one pointer, resolved at runtime instead of compile time.fn notify_dynamic(item: &dyn Summary) {println!("Breaking news! {}", item.summarize());}fn main() {let tweet = create_tweet();notify(&tweet);notify_dynamic(&tweet);}
impl Trait sugar for accepting or returning 'some type that implements Summary' with static dispatch, contrasted with dyn Trait, which erases the concrete type so callers can be resolved at runtime instead.
🧠 Check your understanding
0/1 · 0/1 answered1. Why does fn largest<T: PartialOrd>(list: &[T]) -> &T need the trait bound PartialOrd, instead of compiling for any T?