Lesson 30 of 94
Deriving Standard Traits
Rust has no universal base type. Each of these abilities is a separate standard trait, and a type has only the ones it implements.
Most of the time you don't write them by hand: #[derive(...)] generates a correct implementation from the type's fields. This lesson shows each trait, what derive generates, and when to write your own.
| Rust trait | Derivable? | Used by |
|---|---|---|
Debug |
yes | {:?}, dbg!, assert_eq! |
Display |
no | {}, .to_string() |
Clone |
yes | .clone() |
Copy |
yes | implicit copies |
PartialEq, Eq |
yes | ==, != |
PartialOrd, Ord |
yes | <, >, sort(), max() |
Hash |
yes | HashMap, HashSet keys |
Default |
yes | T::default(), ..Default::default() |
Drop |
no | runs automatically at end of scope |
1. Debug: printing for developers
#[derive(Debug)] prints every field. {:#?} pretty-prints.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug)]
enum Shape {
Circle { center: Point, radius: u32 },
Line(Point, Point),
}
fn main() {
let s = Shape::Line(Point { x: 0, y: 0 }, Point { x: 3, y: 4 });
println!("{:?}", s);
println!("{:#?}", Shape::Circle { center: Point { x: 1, y: 1 }, radius: 5 });
}Line(Point { x: 0, y: 0 }, Point { x: 3, y: 4 })
Circle {
center: Point {
x: 1,
y: 1,
},
radius: 5,
}Deriving Debug requires every field's type to implement Debug, which all standard types do.
2. A hand-written Debug that hides secrets
A derived Debug would print passwords and tokens into your logs. Write it by hand to redact them. debug_struct builds the same format the derive produces.
use std::fmt;
struct DbConfig {
host: String,
user: String,
password: String,
}
impl fmt::Debug for DbConfig {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("DbConfig")
.field("host", &self.host)
.field("user", &self.user)
.field("password", &"********")
.finish()
}
}
fn main() {
let c = DbConfig { host: String::from("db.internal"), user: String::from("app"), password: String::from("hunter2") };
println!("{:?}", c);
}DbConfig { host: "db.internal", user: "app", password: "********" }3. Display: printing for users
Display can't be derived, because only you know how a type should look to users. Implementing Display also gives you .to_string() for free.
use std::fmt;
struct Temperature {
celsius: f64,
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
fn main() {
let t = Temperature { celsius: 21.456 };
println!("Now: {t}");
let label: String = t.to_string();
println!("{} bytes", label.len());
}Now: 21.5°C
7 bytes4. Display that respects width and alignment
write! ignores format options such as {:>12}. f.pad(...) applies them, so your type lines up in tables like a built-in type.
use std::fmt;
struct Sku(u32);
impl fmt::Display for Sku {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(&format!("SKU-{:05}", self.0))
}
}
fn main() {
println!("[{}]", Sku(42));
println!("[{:>14}]", Sku(42));
println!("[{:<14}]", Sku(7));
println!("[{:^14}]", Sku(1234));
}[SKU-00042]
[ SKU-00042]
[SKU-00007 ]
[ SKU-01234 ]5. Clone: explicit deep copies
#[derive(Clone)] clones each field. It requires every field to be Clone.
#[derive(Debug, Clone)]
struct Draft {
title: String,
tags: Vec<String>,
}
fn main() {
let original = Draft { title: String::from("Plan"), tags: vec![String::from("q3")] };
let mut copy = original.clone();
copy.tags.push(String::from("urgent"));
println!("{:?}\n{:?}", original, copy);
}Draft { title: "Plan", tags: ["q3"] }
Draft { title: "Plan", tags: ["q3", "urgent"] }6. Copy requires every field to be Copy
Copy is a marker that says "duplicating my bits is a complete copy". It needs Clone too, and fails if any field owns heap data.
#[derive(Clone, Copy)]
struct Label {
id: u32,
text: String,
}
fn main() {}error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:1:17
|
1 | #[derive(Clone, Copy)]
| ^^^^
...
4 | text: String,
| ------------ this field does not implement `Copy`7. PartialEq: == field by field
The derived == compares all fields in order.
#[derive(Debug, PartialEq)]
struct Address {
street: String,
city: String,
}
#[derive(Debug, PartialEq)]
enum Status {
Active,
Suspended { reason: String },
}
fn main() {
let a = Address { street: String::from("1 Main St"), city: String::from("Springfield") };
let b = Address { street: String::from("1 Main St"), city: String::from("Springfield") };
println!("{}", a == b);
println!("{}", Status::Active == Status::Suspended { reason: String::from("spam") });
println!("{}", Status::Suspended { reason: String::from("x") } == Status::Suspended { reason: String::from("x") });
}true
false
true8. A hand-written PartialEq: equality by ID
Entities are often equal when their IDs match, regardless of other fields.
struct Customer {
id: u64,
name: String,
last_seen: u64,
}
impl PartialEq for Customer {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
fn main() {
let before = Customer { id: 7, name: String::from("Ana"), last_seen: 100 };
let after = Customer { id: 7, name: String::from("Ana Silva"), last_seen: 250 };
println!("same customer: {}", before == after);
println!("{} -> {} (seen {} -> {})", before.name, after.name, before.last_seen, after.last_seen);
}same customer: true
Ana -> Ana Silva (seen 100 -> 250)9. Comparing with a different type
PartialEq can compare with another type, for example your enum against a string.
#[derive(Debug)]
enum Env {
Dev,
Prod,
}
impl PartialEq<&str> for Env {
fn eq(&self, other: &&str) -> bool {
matches!((self, *other), (Env::Dev, "dev") | (Env::Prod, "prod"))
}
}
fn main() {
let current = Env::Prod;
println!("{}", current == "prod");
println!("{}", Env::Dev == "prod");
}true
false10. Eq: equality that is reflexive
Eq adds a promise to PartialEq: every value equals itself. Floats break that promise, because NaN != NaN. That's why f64 is PartialEq but not Eq, and why a struct with an f64 field can't derive Eq.
#[derive(PartialEq, Eq)]
struct Reading {
sensor: u32,
value: f64,
}
fn main() {}error[E0277]: the trait bound `f64: Eq` is not satisfied
--> src/main.rs:4:5
|
1 | #[derive(PartialEq, Eq)]
| -- in this derive macro expansion
...
4 | value: f64,
| ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`
|
= help: the following other types implement trait `Eq`:
i128
i16
i32
i64
i8
isize
u128
u16
and 4 others
note: required by a bound in `AssertParamIsEq`
--> /rustc/<version>/library/core/src/cmp.rs:371:0Eq matters because hash maps and sets require it for their keys.
11. PartialOrd and Ord: ordering
Deriving the ordering traits compares fields in declaration order, like comparing tuples. For enums, earlier variants are smaller. Ord requires Eq and PartialOrd.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Severity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
println!("{}", Severity::Critical > Severity::Medium);
let mut levels = vec![Severity::High, Severity::Low, Severity::Critical];
levels.sort();
println!("{:?}", levels);
let a = Version { major: 1, minor: 10, patch: 0 };
let b = Version { major: 1, minor: 9, patch: 42 };
println!("{:?} > {:?}: {}", a, b, a > b);
}true
[Low, High, Critical]
Version { major: 1, minor: 10, patch: 0 } > Version { major: 1, minor: 9, patch: 42 }: true12. Sorting structs requires Ord
sort() needs a total order. Without Ord the call doesn't compile.
struct Task {
priority: u8,
title: String,
}
fn main() {
let mut tasks = vec![Task { priority: 2, title: String::from("b") }, Task { priority: 1, title: String::from("a") }];
tasks.sort();
}error[E0277]: the trait bound `Task: Ord` is not satisfied
--> src/main.rs:8:11
|
8 | tasks.sort();
| ^^^^ the trait `Ord` is not implemented for `Task`
|
note: required by a bound in `slice::<impl [T]>::sort`
--> /rustc/<version>/library/alloc/src/slice.rs:131:4
help: consider annotating `Task` with `#[derive(Ord)]`
|
1 + #[derive(Ord)]
2 | struct Task {
|13. A hand-written Ord: custom sort order
Implement cmp to define the order yourself. Ordering is an enum with Less, Equal, and Greater, and then_with breaks ties. Here, higher priority comes first, and then titles sort alphabetically.
use std::cmp::Ordering;
#[derive(Debug, PartialEq, Eq)]
struct Task {
priority: u8,
title: String,
}
impl Ord for Task {
fn cmp(&self, other: &Self) -> Ordering {
other.priority.cmp(&self.priority) // reversed: higher first
.then_with(|| self.title.cmp(&other.title))
}
}
impl PartialOrd for Task {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn main() {
let mut tasks = vec![
Task { priority: 1, title: String::from("water plants") },
Task { priority: 3, title: String::from("fix prod bug") },
Task { priority: 3, title: String::from("call customer") },
Task { priority: 2, title: String::from("review PR") },
];
tasks.sort();
for t in &tasks {
println!("{} {}", t.priority, t.title);
}
}3 call customer
3 fix prod bug
2 review PR
1 water plantsKeep PartialEq consistent with Ord: here both are derived from, or compare, the same fields. partial_cmp simply delegates to cmp; that is the standard way to write it.
14. min, max, and clamp work on any Ord type
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Tier {
Free,
Pro,
Enterprise,
}
fn main() {
let user = Tier::Enterprise;
let plan_limit = Tier::Pro;
println!("{:?}", user.min(plan_limit));
println!("{:?}", Tier::Free.max(Tier::Pro));
println!("{:?}", Tier::Enterprise.clamp(Tier::Free, Tier::Pro));
let tiers = [Tier::Pro, Tier::Free, Tier::Enterprise];
println!("{:?}", tiers.iter().max());
}Pro
Pro
Pro
Some(Enterprise)15. Floats and ordering
f64 is only PartialOrd, because NaN has no order. sort() on a Vec<f64> doesn't compile. sort_by(f64::total_cmp) uses a total order that defines where NaN goes.
fn main() {
let mut prices = vec![19.99, 4.5, f64::NAN, 100.0, -1.0];
prices.sort_by(f64::total_cmp);
println!("{:?}", prices);
println!("{:?}", 3.0_f64.partial_cmp(&f64::NAN));
}[-1.0, 4.5, 19.99, 100.0, NaN]
None16. Hash: using values as hash keys
#[derive(Hash)] hashes every field. Types used as keys in a HashMap or elements of a HashSet need Hash and Eq. Collections are covered in HashMap and HashSet.
use std::collections::HashSet;
#[derive(Debug, PartialEq, Eq, Hash)]
struct Coordinate {
x: i32,
y: i32,
}
fn main() {
let mut visited = HashSet::new();
let path = [(0, 0), (1, 0), (1, 1), (0, 0), (1, 0)];
for (x, y) in path {
let first_time = visited.insert(Coordinate { x, y });
println!("({x}, {y}) first visit: {first_time}");
}
println!("{} distinct cells", visited.len());
}(0, 0) first visit: true
(1, 0) first visit: true
(1, 1) first visit: true
(0, 0) first visit: false
(1, 0) first visit: false
3 distinct cells17. A hand-written Hash must agree with Eq
The rule is: if a == b, then hash(a) == hash(b). When you write PartialEq by hand, write Hash by hand over the same data.
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
#[derive(Debug)]
struct Email(String);
impl PartialEq for Email {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for Email {}
impl Hash for Email {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_ascii_lowercase().hash(state);
}
}
fn main() {
let mut subscribers = HashSet::new();
for e in ["Ana@Example.com", "ben@example.com", "ana@example.COM"] {
if !subscribers.insert(Email(e.to_string())) {
println!("duplicate: {e}");
}
}
println!("{} unique subscribers", subscribers.len());
}duplicate: ana@example.COM
2 unique subscribersimpl Eq for Email {} has no body: Eq only adds a promise. fn hash<H: Hasher> is a generic method; Generics comes next.
18. Default: a sensible starting value
Derived Default uses each field's default (0, false, empty collections, None).
#[derive(Debug, Default)]
struct SearchQuery {
text: String,
page: u32,
page_size: u32,
include_archived: bool,
tag: Option<String>,
}
fn main() {
let q = SearchQuery::default();
println!("{:?}", q);
let custom = SearchQuery { text: String::from("rust"), page_size: 50, ..Default::default() };
println!("{:?}", custom);
}SearchQuery { text: "", page: 0, page_size: 0, include_archived: false, tag: None }
SearchQuery { text: "rust", page: 0, page_size: 50, include_archived: false, tag: None }19. A hand-written Default for non-zero defaults
When zero is the wrong default, implement it yourself.
#[derive(Debug)]
struct RetryPolicy {
max_attempts: u32,
base_delay_ms: u64,
jitter: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self { max_attempts: 3, base_delay_ms: 200, jitter: true }
}
}
fn main() {
println!("{:?}", RetryPolicy::default());
println!("{:?}", RetryPolicy { max_attempts: 10, ..Default::default() });
}RetryPolicy { max_attempts: 3, base_delay_ms: 200, jitter: true }
RetryPolicy { max_attempts: 10, base_delay_ms: 200, jitter: true }20. Default for enums with #[default]
#[derive(Debug, Default)]
enum LogLevel {
Debug,
#[default]
Info,
Warn,
}
#[derive(Debug, Default)]
struct Logger {
level: LogLevel,
prefix: String,
}
fn main() {
println!("{:?}", Logger::default());
let _ = (LogLevel::Debug, LogLevel::Warn);
}Logger { level: Info, prefix: "" }21. Drop: code that runs when a value is dropped
Drop is Rust's AutoCloseable, except that you never call it: the compiler inserts the call when the owner goes out of scope. This is called RAII (resource acquisition is initialization).
struct Connection {
name: &'static str,
}
impl Drop for Connection {
fn drop(&mut self) {
println!("closing {}", self.name);
}
}
fn main() {
let _a = Connection { name: "db" };
{
let _b = Connection { name: "cache" };
println!("using cache");
}
println!("using db");
}using cache
closing cache
using db
closing dbIn Rust, cleanup cannot be forgotten.
22. Drop order: reverse order of declaration
This is the example promised in Ownership. Values in a scope are dropped last-first, and a struct's fields are dropped after the struct itself, in declaration order.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Pair {
first: Noisy,
second: Noisy,
}
impl Drop for Pair {
fn drop(&mut self) {
println!("drop pair");
}
}
fn main() {
let _config = Noisy("config");
let _connection = Noisy("connection");
let _pair = Pair { first: Noisy("pair.first"), second: Noisy("pair.second") };
let items = vec![Noisy("vec[0]"), Noisy("vec[1]")];
drop(items); // dropped early, elements in order
println!("end of main");
}drop vec[0]
drop vec[1]
end of main
drop pair
drop pair.first
drop pair.second
drop connection
drop config23. You can't call drop yourself as a method
Calling x.drop() directly would let x be dropped twice. Use the free function drop(x), which takes ownership.
struct File;
impl Drop for File {
fn drop(&mut self) {
println!("closed");
}
}
fn main() {
let f = File;
f.drop();
}error[E0040]: explicit use of destructor method
--> src/main.rs:11:7
|
11 | f.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
11 - f.drop();
11 + drop(f);
|24. A type can't be both Copy and Drop
If a type could be copied implicitly, it would be unclear how many times its cleanup should run. The compiler forbids the combination.
#[derive(Clone, Copy)]
struct Handle(u32);
impl Drop for Handle {
fn drop(&mut self) {}
}
fn main() {}error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor
--> src/main.rs:1:17
|
1 | #[derive(Clone, Copy)]
| ^^^^ `Copy` not allowed on types with destructors25. Realistic example: a database transaction guard
A guard rolls back automatically unless it is explicitly committed. Early returns and errors can't leave a transaction open. commit(self) consumes the guard, so it can't be used afterwards.
struct Transaction<'a> {
log: &'a mut Vec<String>,
committed: bool,
}
impl<'a> Transaction<'a> {
fn begin(log: &'a mut Vec<String>) -> Self {
log.push(String::from("BEGIN"));
Self { log, committed: false }
}
fn execute(&mut self, sql: &str) {
self.log.push(sql.to_string());
}
fn commit(mut self) {
self.log.push(String::from("COMMIT"));
self.committed = true;
}
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
if !self.committed {
self.log.push(String::from("ROLLBACK"));
}
}
}
fn transfer(log: &mut Vec<String>, amount: i64) -> Result<(), String> {
let mut tx = Transaction::begin(log);
tx.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
if amount > 1_000 {
return Err(String::from("limit exceeded")); // tx dropped here: ROLLBACK
}
tx.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
tx.commit();
Ok(())
}
fn main() {
let mut log = Vec::new();
println!("{:?}", transfer(&mut log, 100));
println!("{:?}", transfer(&mut log, 5_000));
for line in &log {
println!(" {line}");
}
}Ok(())
Err("limit exceeded")
BEGIN
UPDATE accounts SET balance = balance - 100 WHERE id = 1
UPDATE accounts SET balance = balance + 100 WHERE id = 2
COMMIT
BEGIN
UPDATE accounts SET balance = balance - 100 WHERE id = 1
ROLLBACK26. Realistic example: a semantic version type
Derived ordering compares major, then minor, then patch, which is exactly semantic version ordering. Display formats it, and Hash lets it be a map key.
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct SemVer {
major: u32,
minor: u32,
patch: u32,
}
impl fmt::Display for SemVer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl SemVer {
fn new(major: u32, minor: u32, patch: u32) -> Self {
Self { major, minor, patch }
}
fn is_compatible_with(&self, required: &SemVer) -> bool {
self.major == required.major && self >= required
}
}
fn main() {
let mut releases = vec![SemVer::new(1, 10, 0), SemVer::new(1, 2, 3), SemVer::new(2, 0, 0), SemVer::new(1, 9, 9)];
releases.sort();
let listed: Vec<String> = releases.iter().map(|v| v.to_string()).collect();
println!("{}", listed.join(" < "));
let required = SemVer::new(1, 9, 0);
for r in &releases {
println!("{r} satisfies ^{}: {}", required.to_string().trim_start_matches('v'), r.is_compatible_with(&required));
}
}v1.2.3 < v1.9.9 < v1.10.0 < v2.0.0
v1.2.3 satisfies ^1.9.0: false
v1.9.9 satisfies ^1.9.0: true
v1.10.0 satisfies ^1.9.0: true
v2.0.0 satisfies ^1.9.0: false27. Realistic example: a secret wrapper type
Wrap sensitive strings in a type whose Debug and Display never show the value. Accidentally logging a whole config struct is then harmless.
use std::fmt;
#[derive(Clone, PartialEq)]
struct Secret(String);
impl Secret {
fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Secret(***)")
}
}
impl fmt::Display for Secret {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("***")
}
}
#[derive(Debug, Clone)]
struct ApiConfig {
endpoint: String,
api_key: Secret,
}
fn main() {
let config = ApiConfig { endpoint: String::from("https://api.example.com"), api_key: Secret(String::from("sk_live_51H8x")) };
println!("{:?}", config);
println!("key: {}", config.api_key);
println!("header: Bearer {}...", &config.api_key.expose()[..7]);
}ApiConfig { endpoint: "https://api.example.com", api_key: Secret(***) }
key: ***
header: Bearer sk_live...28. Realistic example: an incident list sorted by several keys
Derived Ord on an enum plus a hand-written Ord on the struct gives a precise, reusable sort order.
use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Severity {
Minor,
Major,
Critical,
}
#[derive(Debug, PartialEq, Eq)]
struct Incident {
severity: Severity,
opened_at: u64,
title: String,
}
impl Ord for Incident {
fn cmp(&self, other: &Self) -> Ordering {
other.severity.cmp(&self.severity) // most severe first
.then(self.opened_at.cmp(&other.opened_at)) // then oldest first
.then_with(|| self.title.cmp(&other.title))
}
}
impl PartialOrd for Incident {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Incident {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:<9} t={:<5} {}", format!("{:?}", self.severity), self.opened_at, self.title)
}
}
fn main() {
let mut incidents = vec![
Incident { severity: Severity::Major, opened_at: 300, title: String::from("slow checkout") },
Incident { severity: Severity::Critical, opened_at: 500, title: String::from("payments down") },
Incident { severity: Severity::Minor, opened_at: 100, title: String::from("typo on homepage") },
Incident { severity: Severity::Major, opened_at: 200, title: String::from("search timeouts") },
Incident { severity: Severity::Critical, opened_at: 450, title: String::from("login failures") },
];
incidents.sort();
for i in &incidents {
println!("{i}");
}
}Critical t=450 login failures
Critical t=500 payments down
Major t=200 search timeouts
Major t=300 slow checkout
Minor t=100 typo on homepageSummary
- Derive
Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Hash, andDefaultwhenever the field-by-field behavior is right, which is most of the time. - Implement by hand when you need something different: redacted
Debug, ID-based equality, a custom sort order, non-zero defaults. Displayis always hand-written. It gives.to_string()for free. Usef.padto respect width and alignment.- Keep
EqandHashconsistent, andPartialEqandOrdconsistent. f64isPartialEqandPartialOrdonly, because ofNaN. Usetotal_cmpto sort floats.Dropruns cleanup automatically at the end of scope, in reverse order of creation. It can't be combined withCopy.
Next: Generics.
