03 · Traits & Generics¶
Traits and generics are how Rust writes code once and reuses it across many
types, without giving up compile-time checking or paying a runtime cost for
the flexibility. A trait describes behavior a type can implement (like an
interface); a generic lets a function or struct work over any type
that satisfies a given trait. Together they're the backbone of idiomatic
Rust — you've already used them indirectly every time you called .clone()
or println!("{:?}", ...).
Defining and implementing a trait¶
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
headline: String,
body: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}...", self.headline, &self.body[..20.min(self.body.len())])
}
}
struct Tweet {
username: String,
text: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.text)
}
}
fn main() {
let article = Article {
headline: String::from("Rust 2.0 announced"),
body: String::from("The Rust team today announced a major release."),
};
let tweet = Tweet {
username: String::from("ferris"),
text: String::from("traits are awesome"),
};
println!("{}", article.summarize());
println!("{}", tweet.summarize());
}
Any type can implement any trait, as long as either the trait or the type is defined in your own crate (the "orphan rule" — it prevents two unrelated crates from both implementing the same foreign trait for the same foreign type, which would create an ambiguity nobody could resolve).
Default method implementations¶
trait Summary {
fn title(&self) -> String;
// A default implementation -- types can use this as-is or override it
fn summarize(&self) -> String {
format!("(Read more about {}...)", self.title())
}
}
struct Article {
headline: String,
}
impl Summary for Article {
fn title(&self) -> String {
self.headline.clone()
}
// no summarize() override -- uses the default
}
fn main() {
let article = Article { headline: String::from("Rust 2.0 announced") };
println!("{}", article.summarize());
}
Trait bounds: generic functions that require behavior¶
A generic function parameter like <T> means "works for any type" — but
"any type" alone doesn't let you call .summarize() inside the function,
because the compiler doesn't know T has that method. A trait bound
restricts T to types that implement a given trait, which is what unlocks
calling its methods:
trait Summary {
fn summarize(&self) -> String;
}
struct Tweet {
text: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
self.text.clone()
}
}
// `T: Summary` is the trait bound -- T can be any type that implements Summary
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Equivalent shorthand using `impl Trait` in argument position
fn notify_short(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
fn main() {
let tweet = Tweet { text: String::from("Rust ships generics") };
notify(&tweet);
notify_short(&tweet);
}
fn notify<T: Summary>(item: &T) and fn notify_short(item: &impl Summary)
compile to the same thing — impl Trait is sugar for a generic with a bound,
useful when you don't need to name T anywhere else in the signature.
Multiple bounds and where clauses¶
use std::fmt::Debug;
trait Summary {
fn summarize(&self) -> String;
}
// Multiple bounds with `+` -- T must implement both traits
fn notify_and_log<T: Summary + Debug>(item: &T) {
println!("Breaking news! {}", item.summarize());
println!("(debug: {:?})", item);
}
// The same signature, written as a `where` clause instead -- purely
// stylistic here, but it becomes much easier to read than `+`-chains
// once a function has several type parameters or several bounds each:
//
// fn notify_and_log<T>(item: &T) where T: Summary + Debug { ... }
#[derive(Debug)]
struct Tweet {
text: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
self.text.clone()
}
}
fn main() {
let tweet = Tweet { text: String::from("where clauses read better") };
notify_and_log(&tweet);
}
Generic structs¶
struct Pair<T> {
first: T,
second: T,
}
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
fn new(first: T, second: T) -> Pair<T> {
Pair { first, second }
}
fn larger(&self) -> &T {
if self.first >= self.second { &self.first } else { &self.second }
}
}
fn main() {
let ints = Pair::new(5, 12);
println!("larger: {}", ints.larger()); // larger: 12
let floats = Pair::new(3.5, 1.2);
println!("larger: {}", floats.larger()); // larger: 3.5
let words = Pair::new(String::from("apple"), String::from("banana"));
println!("larger: {}", words.larger()); // larger: banana
}
Pair<T> is defined once but the compiler generates a specialized version
for each concrete T it's used with (Pair<i32>, Pair<f64>,
Pair<String>) — this is called monomorphization, and it's why generics
in Rust have no runtime cost: by the time the program runs, there's no
generic code left, only ordinary compiled functions per type.
Trait objects: when you need a collection of different types¶
Generics pick one concrete type per call site — but sometimes you need a
single Vec holding several different types that share a trait. That's
what dyn Trait (a trait object) is for, at the cost of a small runtime
dispatch overhead instead of monomorphization:
trait Summary {
fn summarize(&self) -> String;
}
struct Article { headline: String }
impl Summary for Article {
fn summarize(&self) -> String { format!("Article: {}", self.headline) }
}
struct Tweet { text: String }
impl Summary for Tweet {
fn summarize(&self) -> String { format!("Tweet: {}", self.text) }
}
fn main() {
// Box<dyn Summary> -- a heap-allocated value of some type that impls Summary
let items: Vec<Box<dyn Summary>> = vec![
Box::new(Article { headline: String::from("Rust news") }),
Box::new(Tweet { text: String::from("hello") }),
];
for item in &items {
println!("{}", item.summarize());
}
}
Rule of thumb: reach for generics (<T: Trait>) when you know the concrete
type at compile time and want zero-cost dispatch; reach for dyn Trait when
you need a heterogeneous collection or the type genuinely isn't known until
runtime (like items loaded from a plugin or config).
Common standard-library traits worth knowing¶
| Trait | Purpose | Enables |
|---|---|---|
Debug |
Programmer-facing formatting | {:?} in println! |
Display |
User-facing formatting | {} in println! |
Clone |
Explicit deep copy | .clone() |
PartialEq / Eq |
Equality comparison | ==, != |
PartialOrd / Ord |
Ordering comparison | <, >, .sort() |
Default |
A sensible zero-value | T::default(), ..Default::default() |
Iterator |
Sequential production of values | for loops, adapters (next module) |
How It Actually Works¶
The generics-vs-trait-objects tradeoff above is really a static-vs-dynamic
dispatch tradeoff, and it's worth seeing what each costs in the compiled
binary. Monomorphization means Pair<i32>::method() and
Pair<String>::method() are two entirely separate functions in the final
binary, each with its logic specialized and inlinable against the concrete
type — the compiler can see through the call completely, which is why
generic code can be just as fast as, or faster than, hand-written
type-specific code (inlining across a monomorphized call is routine; across
a trait-object call it usually isn't). The cost is code size: ten
instantiations of a generic function mean roughly ten copies of its
compiled body, which is the real reason large Rust binaries sometimes
surprise people ("binary bloat" from monomorphization).
Box<dyn Summary> takes the opposite trade: dyn Summary is an unsized
type (the compiler doesn't know at compile time whether it's holding an
Article or a Tweet, so it doesn't know the size), which is why it can
only exist behind a pointer like Box, &, or Rc. That pointer is
actually a fat pointer — two words instead of one: a data pointer to the
heap-allocated value, and a pointer to a vtable, a static table of
function pointers generated once per concrete type at compile time
(Article's vtable, Tweet's vtable) holding the addresses of that type's
trait-method implementations. Calling item.summarize() on a trait object
means an indirect call through the vtable — one extra pointer dereference
compared to the direct, inlinable call a monomorphized generic gets. That
single indirect jump is the entire "small runtime dispatch overhead" the
text above is referring to.
Exercise¶
Define a trait trait Shape { fn area(&self) -> f64; fn name(&self) -> &str; }.
Implement it for a struct Circle { radius: f64 } and a
struct Square { side: f64 }. Write a generic function
fn describe<T: Shape>(shape: &T) that prints "{name}: area = {area:.2}".
Then write fn total_area(shapes: &[Box<dyn Shape>]) -> f64 that sums the
areas of a mixed Vec<Box<dyn Shape>> containing both circles and squares,
and print the total in main.