Why this matters
The standard library defines a set of traits that the whole ecosystem relies on: printing, copying, comparing, hashing, default values, conversions, operators. When your types implement them, they work with {:?}, ==, sort, HashMap keys, ? conversions and +, just like built-in types. Most can be generated automatically with #[derive(...)], which you have been writing as a recipe since module 7. This lesson explains what each one means and when it can be derived.
derive
#[derive(Trait1, Trait2)] asks the compiler to write the impl for you, field by field:
#[derive(Debug, Clone, PartialEq, Default)]
struct Config {
name: String,
retries: u32,
verbose: bool,
}
fn main() {
let a = Config { name: String::from("prod"), retries: 3, verbose: false };
let b = a.clone();
let c = Config::default();
println!("{:?}", a);
println!("{} {}", a == b, a == c);
println!("{:?}", c);
let d = Config { retries: 5, ..Default::default() };
println!("{:?}", d);
}
Config { name: "prod", retries: 3, verbose: false }
true false
Config { name: "", retries: 0, verbose: false }
Config { name: "", retries: 5, verbose: false }
A derive only works if every field implements the trait; the generated code calls it on each field.
The traits, one by one
Debug: {:?} formatting, for programmers. Derive it on nearly every type; it costs nothing and makes debugging easier.
Clone: explicit duplication with .clone(). Derived as "clone each field".
Copy: implicit bitwise copy on assignment (module 4). Requires Clone, and every field must be Copy. Only for small, plain data with no heap ownership: points, colors, IDs.
PartialEq / Eq: == and !=. Derived as "all fields equal". Eq adds the promise that every value equals itself, which floats break (NaN != NaN), so types with f64 fields can be PartialEq but not Eq. HashMap keys need Eq.
PartialOrd / Ord: <, >, sort, max, BTreeMap keys. Derived as lexicographic order: compare the first field; if equal, the second; and so on (for enums, earlier variants are smaller). Field order in the struct definition therefore matters. Ord is a total order and requires Eq; floats again only manage Partial.
Hash: lets a value be hashed for HashMap/HashSet. Must agree with Eq (equal values must have equal hashes); deriving both keeps them consistent.
Default: Type::default(), a sensible "empty" value (zeros, empty strings, None, false).
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Suit { Clubs, Diamonds, Hearts, Spades }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Card { rank: u8, suit: Suit }
fn main() {
let mut hand = vec![
Card { rank: 12, suit: Suit::Hearts },
Card { rank: 3, suit: Suit::Spades },
Card { rank: 12, suit: Suit::Clubs },
];
hand.sort();
println!("{:?}", hand);
println!("{}", Suit::Clubs < Suit::Spades);
let unique: HashSet<Card> = hand.iter().copied().collect();
println!("{} unique", unique.len());
}
[Card { rank: 3, suit: Spades }, Card { rank: 12, suit: Clubs }, Card { rank: 12, suit: Hearts }]
true
3 unique
Cards sort by rank first (the first field), then by suit in variant order.
Implementing by hand
Derive when the field-by-field behavior is right; implement manually when it is not. A common case: equality that ignores some fields, or a custom order. Here Display (never derivable, since there is no single right user-facing format) and an Ord that sorts by score descending:
use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, PartialEq, Eq)]
struct Player { name: String, score: u32 }
impl Ord for Player {
fn cmp(&self, other: &Self) -> Ordering {
other.score.cmp(&self.score).then_with(|| self.name.cmp(&other.name))
}
}
impl PartialOrd for Player {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({})", self.name, self.score)
}
}
fn main() {
let mut ps = vec![
Player { name: String::from("bo"), score: 7 },
Player { name: String::from("al"), score: 9 },
Player { name: String::from("cy"), score: 7 },
];
ps.sort();
for p in &ps {
println!("{p}");
}
}
al (9)
bo (7)
cy (7)
cmp returns an Ordering (Less, Equal, Greater); then_with breaks ties. Swapping other and self reverses the order.
Conversions: From and Into
From<T> for U defines how to build a U from a T. Implementing it gives you Into for free (another blanket impl), and ? uses From to convert errors (module 8):
#[derive(Debug)]
struct Celsius(f64);
impl From<f64> for Celsius {
fn from(v: f64) -> Self {
Celsius(v)
}
}
fn main() {
let a = Celsius::from(21.5);
let b: Celsius = 30.0.into();
let s = String::from("text");
let n = i64::from(42i32);
println!("{:?} {:?} {s} {n}", a, b);
}
Celsius(21.5) Celsius(30.0) text 42
From is for conversions that cannot fail. For fallible ones there is TryFrom (u8::try_from(300) returns an Err).
Operators are traits
a + b calls std::ops::Add::add(a, b); a == b calls PartialEq::eq; a[i] calls Index::index. Implementing the trait gives your type the operator:
use std::ops::{Add, Mul};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Money(i64);
impl Add for Money {
type Output = Money;
fn add(self, rhs: Money) -> Money {
Money(self.0 + rhs.0)
}
}
impl Mul<i64> for Money {
type Output = Money;
fn mul(self, n: i64) -> Money {
Money(self.0 * n)
}
}
fn main() {
let price = Money(250);
let total = price * 3 + Money(100);
println!("{:?}", total);
}
Money(850)
type Output = Money; is an associated type: part of the trait that each implementation fills in (here, the result type of +). Iterator's type Item is another example. Overload operators only where the meaning is obvious (math types, money); a surprising + makes code harder to read.
Common mistakes
- Deriving
Copyon a type with aStringfield: E0204 "the traitCopycannot be implemented for this type". - Deriving
Eq,OrdorHashwithf64fields: E0277. Store an integer representation, or implementPartialOrdonly. - Inconsistent manual impls:
Orddisagreeing withPartialOrd, orHashdisagreeing withEq, produces baffling bugs in sorts and maps. Derive when you can; when implementingOrdby hand, definepartial_cmpasSome(self.cmp(other)). - Field order surprises: derived
Ordcompares in declaration order. Reorder fields or implement it by hand.
Recap
#[derive]generates field-by-field impls; every field must implement the trait.Debugfor{:?},Clone/Copyfor duplication,PartialEq/Eqfor==,PartialOrd/Ordfor ordering,Hashfor hashing,Defaultfor empty values.- Floats are only
PartialEq/PartialOrd;Stringis neverCopy. Display, custom orders and conversions are implemented by hand;FromgivesIntofor free.- Operators are traits in
std::opswith associatedOutputtypes.
Going further: read the docs for std::cmp::Ordering and try sort_by_key(|p| std::cmp::Reverse(p.score)) as an alternative to a manual Ord.