Nugget 6
Home
You wrote fn greet_dog(d: &Dog) and now need
fn greet_cat(c: &Cat) — identical except the type.
Trait bounds let you write it once.
Let's reuse the Speak trait from the previous nugget:
trait Speak {
fn make_sound(&self) -> String;
}
struct Dog { name: String }
struct Cat { name: String }
impl Speak for Dog {
fn make_sound(&self) -> String {
format!("{} says Woof!", self.name)
}
}
impl Speak for Cat {
fn make_sound(&self) -> String {
format!("{} says Meow!", self.name)
}
}
Without trait bounds, you'd write a separate function for every type:
fn greet_dog(d: &Dog) {
println!("Hello! {}", d.make_sound());
}
fn greet_cat(c: &Cat) {
println!("Hello! {}", c.make_sound());
}
// And another for every new animal...
Use <T: Speak> to say "for any type T that implements Speak":
fn greet<T: Speak>(animal: &T) {
println!("Hello! {}", animal.make_sound());
}
fn main() {
let dog = Dog { name: "Rex".into() };
let cat = Cat { name: "Whiskers".into() };
greet(&dog); // ✅ Hello! Rex says Woof!
greet(&cat); // ✅ Hello! Whiskers says Meow!
}
One function works for any type that implements Speak.
Add a Parrot tomorrow — greet() works with no changes.
A generic parameter can require multiple traits with the
+ syntax:
use std::fmt::Debug;
// Requires both Speak and Debug
fn announce<T: Speak + Debug>(animal: &T) {
dbg!(animal); // Debug
println!("{}", animal.make_sound()); // Speak
}
#[derive(Debug)]
struct Dog { name: String }
impl Speak for Dog { /* as before */ }
fn main() {
let dog = Dog { name: "Rex".into() };
announce(&dog);
}
💡 The where clause
For long bounds, the where syntax is cleaner:
fn announce<T>(animal: &T)
where
T: Speak + Debug + Clone + Display,
{ /* ... */ }
When you write greet::<Dog>(&dog), the compiler generates a
specialized version of greet just for Dog
with the method calls resolved at compile time. This is called
monomorphization — zero runtime overhead:
// What you write:
fn greet<T: Speak>(animal: &T) { animal.make_sound(); }
// What the compiler generates for Dog:
fn greet__Dog(animal: &Dog) { Dog::make_sound(animal); }
// What the compiler generates for Cat:
fn greet__Cat(animal: &Cat) { Cat::make_sound(animal); }
📐 Zero-cost abstraction
Generic functions with trait bounds have zero runtime overhead compared to writing type-specific functions. The compiler creates a separate copy for each concrete type used, with all method calls resolved at compile time.
| Bound | What it enables | Example |
|---|---|---|
T: Debug |
{:?} formatting |
fn log<T: Debug>(val: &T) |
T: Display |
{} formatting + .to_string() |
fn print<T: Display>(val: &T) |
T: Clone |
Explicit .clone() |
fn duplicate<T: Clone>(val: T) -> T |
T: PartialEq |
== and != |
fn find<T: PartialEq>(list: &[T], val: &T) |
T: Iterator |
for item in items |
fn sum<T: Iterator<Item=i32>>(it: T) |
fn foo<T: Trait>(val: &T) — one function works for any type implementing Trait.+: <T: Speak + Debug>.where for readability when bounds get long.