Guide contents

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.

Rust
#[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 });
}
Output
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.

Rust
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);
}
Output
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.

Rust
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());
}
Output
Now: 21.5°C
7 bytes

4. 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.

Rust
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));
}
Output
[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.

Rust
#[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);
}
Output
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.

Rust✗ does not compile · E0204
#[derive(Clone, Copy)]
struct Label {
    id: u32,
    text: String,
}

fn main() {}
Output
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.

Rust
#[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") });
}
Output
true
false
true

8. A hand-written PartialEq: equality by ID

Entities are often equal when their IDs match, regardless of other fields.

Rust
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);
}
Output
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.

Rust
#[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");
}
Output
true
false

10. 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.

Rust✗ does not compile · E0277
#[derive(PartialEq, Eq)]
struct Reading {
    sensor: u32,
    value: f64,
}

fn main() {}
Output
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:0

Eq 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.

Rust
#[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);
}
Output
true
[Low, High, Critical]
Version { major: 1, minor: 10, patch: 0 } > Version { major: 1, minor: 9, patch: 42 }: true

12. Sorting structs requires Ord

sort() needs a total order. Without Ord the call doesn't compile.

Rust✗ does not compile · E0277
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();
}
Output
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.

Rust
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);
    }
}
Output
3 call customer
3 fix prod bug
2 review PR
1 water plants

Keep 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

Rust
#[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());
}
Output
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.

Rust
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));
}
Output
[-1.0, 4.5, 19.99, 100.0, NaN]
None

16. 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.

Rust
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());
}
Output
(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 cells

17. 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.

Rust
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());
}
Output
duplicate: ana@example.COM
2 unique subscribers

impl 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).

Rust
#[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);
}
Output
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.

Rust
#[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() });
}
Output
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]

Rust
#[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);
}
Output
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).

Rust
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");
}
Output
using cache
closing cache
using db
closing db

In 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.

Rust
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");
}
Output
drop vec[0]
drop vec[1]
end of main
drop pair
drop pair.first
drop pair.second
drop connection
drop config

23. 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.

Rust✗ does not compile · E0040
struct File;

impl Drop for File {
    fn drop(&mut self) {
        println!("closed");
    }
}

fn main() {
    let f = File;
    f.drop();
}
Output
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.

Rust✗ does not compile · E0184
#[derive(Clone, Copy)]
struct Handle(u32);

impl Drop for Handle {
    fn drop(&mut self) {}
}

fn main() {}
Output
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 destructors

25. 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.

Rust
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}");
    }
}
Output
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
  ROLLBACK

26. 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.

Rust
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));
    }
}
Output
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: false

27. 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.

Rust
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]);
}
Output
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.

Rust
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}");
    }
}
Output
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 homepage

Summary

  • Derive Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, and Default whenever 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.
  • Display is always hand-written. It gives .to_string() for free. Use f.pad to respect width and alignment.
  • Keep Eq and Hash consistent, and PartialEq and Ord consistent.
  • f64 is PartialEq and PartialOrd only, because of NaN. Use total_cmp to sort floats.
  • Drop runs cleanup automatically at the end of scope, in reverse order of creation. It can't be combined with Copy.

Next: Generics.