Traits & Generics 🧬
Welcome to Level 3. First tool: writing code that works for many types at once, without losing an ounce of safety or speed.
Traits: shared abilities
A trait describes something types can do. Think of it as a badge 🎖️: "this type can be summarized," "this type can be compared," "this type can be printed." You define the badge; any type can earn it:
trait Describe {
fn describe(&self) -> String;
// traits can include default behavior, too
fn shout(&self) -> String {
self.describe().to_uppercase() + "!!!"
}
}
struct Crab { name: String }
struct Wave { height_m: f64 }
impl Describe for Crab {
fn describe(&self) -> String {
format!("a crab named {}", self.name)
}
}
impl Describe for Wave {
fn describe(&self) -> String {
format!("a {}m wave", self.height_m)
}
}
fn main() {
let rusty = Crab { name: String::from("Rusty") };
let big = Wave { height_m: 3.2 };
println!("{}", rusty.describe());
println!("{}", big.shout()); // default method, free of charge
}
Two completely unrelated types now share a vocabulary. If a type skips a required method, the compiler refuses the badge: a half-implemented interface can't exist.
Functions that accept "anything with the badge"
fn announce(item: &impl Describe) {
println!("📣 Behold: {}", item.describe());
}
fn main() {
announce(&Crab { name: String::from("Rusty") });
announce(&Wave { height_m: 1.5 });
}
&impl Describe reads exactly as it sounds: "a borrow of anything
that implements Describe." One function, every describable type, forever,
including types other people write next year.
Generics: type placeholders
Sometimes the flexibility isn't about behavior but about containers.
You've been using generics all along: Vec<T>,
Option<T>, Result<T, E>. The T is
a placeholder filled in per use: Vec<i32>,
Option<String>. You can write your own:
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
fn main() {
let numbers = Pair { first: 1, second: 2 }; // Pair<i32>
let words = Pair { first: "sea", second: "shell" }; // Pair<&str>
println!("{numbers:?} {words:?}");
}
Trait bounds: placeholders with standards
Now combine them. A generic function can demand its placeholder have a badge. This is the pattern that unlocks most of Rust's standard library:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in list {
if item > biggest { // > only works because T: PartialOrd
biggest = item;
}
}
biggest
}
fn main() {
println!("{}", largest(&[3, 7, 2])); // works on numbers
println!("{}", largest(&['x', 'a', 'm'])); // and chars
println!("{}", largest(&["kelp", "algae"])); // and strings
}
T: PartialOrd means "any type T that can be compared with
< and >." Remove the bound and the compiler
rejects the >: it won't let you assume abilities a type might
not have. Maximum flexibility, zero leaps of faith.
largest for each type you use it with, as if you'd hand-written an
i32 version and a char version. Generic code runs
exactly as fast as duplicated code. This is called
zero-cost abstraction, and it's a core reason Rust wins benchmarks.
Badges you've already met
| Trait | Grants | Usually earned by |
|---|---|---|
Debug | printing with {:?} | #[derive(Debug)] |
Clone | .clone() | #[derive(Clone)] |
Copy | copy instead of move (Lesson 6!) | #[derive(Copy, Clone)], small types only |
PartialEq | == and != | #[derive(PartialEq)] |
PartialOrd | < > comparisons | #[derive(PartialOrd)] |
Display | printing with {} | written by hand; you choose the look |
That mysterious #[derive(...)] from Lesson 8 was trait
implementation all along: the compiler writing badge paperwork for you.
- Using
>,==or{:?}on a genericTwithout the bound that grants it. - Forgetting
forinimpl Describe for Crab. - Deriving
Copyon heap-owning types likeString: not allowed, and the compiler explains why.
Noise-makers
Define a trait MakesSound with fn sound(&self) -> String.
Implement it for structs Dog and Robot, then write
fn broadcast(thing: &impl MakesSound) that prints the sound three times.
Reveal solution
trait MakesSound {
fn sound(&self) -> String;
}
struct Dog;
struct Robot;
impl MakesSound for Dog {
fn sound(&self) -> String { String::from("woof") }
}
impl MakesSound for Robot {
fn sound(&self) -> String { String::from("beep-boop") }
}
fn broadcast(thing: &impl MakesSound) {
let s = thing.sound();
println!("{s} {s} {s}!");
}
fn main() {
broadcast(&Dog);
broadcast(&Robot);
}
Generic swap
Write fn swap<T>(pair: (T, T)) -> (T, T) that returns the tuple
reversed. Test it with numbers and with &strs. Did you need
any trait bound? Why not?
Reveal solution
fn swap<T>(pair: (T, T)) -> (T, T) {
(pair.1, pair.0)
}
fn main() {
println!("{:?}", swap((1, 2))); // (2, 1)
println!("{:?}", swap(("hi", "yo"))); // ("yo", "hi")
}
No bound needed: swapping only moves values around. Bounds are for when you use abilities: compare, print, clone…
A proper Display
Give Lesson 8's Player the Display badge by hand, so
println!("{p}") prints CrabLord99 (level 7). Skeleton:
use std::fmt;
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "...") // your format here
}
}
Reveal solution
use std::fmt;
struct Player {
name: String,
level: u32,
}
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} (level {})", self.name, self.level)
}
}
fn main() {
let p = Player { name: String::from("CrabLord99"), level: 7 };
println!("{p}"); // CrabLord99 (level 7)
}