Skip to content
AgileLogics
5 min read

Common Standard Traits

Prerequisites: Traits

The standard library defines a handful of traits that almost every type ends up implementing. Most can be derived with #[derive(...)], which writes the obvious implementation for you, as long as every field also implements the trait.

Trait Gives you Derivable
Debug {:?} and {:#?} formatting yes
Clone an explicit .clone() copy yes
Copy implicit copies instead of moves (needs Clone) yes
PartialEq / Eq == and !=; Eq promises x == x is always true yes
PartialOrd / Ord <, >, .cmp(), sorting; Ord is a total order yes
Hash use as a HashMap key or HashSet member (with Eq) yes
Default Type::default(), a "zero" value yes
Display {} formatting for users no, write it yourself
From / Into conversions between types no, write it yourself

A few rules follow from the table:

  • Copy is only allowed when every field is Copy. A struct holding a String or Vec can be Clone but not Copy.
  • f32/f64 implement PartialEq and PartialOrd but not Eq, Ord or Hash, because NaN != NaN. Structs containing floats inherit that limit.
  • Derived PartialOrd/Ord compare fields in declaration order, and enum variants in the order they are written.
  • Implement From<A> for B and you get Into<B> for A automatically, so implement From, not Into.

Syntax

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Version {
    major: u32,
    minor: u32,
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "v{}.{}", self.major, self.minor)
    }
}

impl From<(u32, u32)> for Version {
    fn from((major, minor): (u32, u32)) -> Self {
        Version { major, minor }
    }
}

Example

use std::collections::HashSet;
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Version {
    major: u32,
    minor: u32,
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "v{}.{}", self.major, self.minor)
    }
}

impl From<(u32, u32)> for Version {
    fn from((major, minor): (u32, u32)) -> Self {
        Version { major, minor }
    }
}

#[derive(Debug, Clone, Default)]
struct Config {
    name: String,
    retries: u8,
    verbose: bool,
}

fn main() {
    let a = Version { major: 1, minor: 4 };
    let b = a;                                 // Copy: `a` is still usable
    println!("{a} == {b}: {}", a == b);

    let c: Version = (1, 10).into();           // Into, provided by our From impl
    let d = Version::from((0, 9));
    println!("{a} < {c}: {}", a < c);

    let mut versions = vec![c, a, d, Version::default()];
    versions.sort();                            // uses Ord
    let names: Vec<String> = versions.iter().map(|v| v.to_string()).collect();
    println!("sorted: {}", names.join(", "));
    println!("max: {}", versions.iter().max().unwrap());

    let unique: HashSet<Version> = [a, b, c].into_iter().collect(); // uses Hash + Eq
    println!("unique versions: {}", unique.len());

    let base = Config { name: String::from("app"), ..Default::default() };
    let copy = base.clone();                   // Clone: an explicit deep copy
    println!("{copy:?}");
    println!("{} retries {} times, verbose: {}", base.name, base.retries, base.verbose);
    println!("{:#?}", Version { major: 2, minor: 0 });
}

Expected output

v1.4 == v1.4: true
v1.4 < v1.10: true
sorted: v0.0, v0.9, v1.4, v1.10
max: v1.10
unique versions: 2
Config { name: "app", retries: 0, verbose: false }
app retries 0 times, verbose: false
Version {
    major: 2,
    minor: 0,
}

Note that v1.4 < v1.10 is true because the fields are compared as numbers (4 < 10), not as text. ..Default::default() fills every field not written out with its default value.

Common mistakes

  • Deriving Copy on a struct with a String field: error[E0204]: the trait `Copy` cannot be implemented for this type. Derive Clone only and call .clone() when needed.
  • Deriving Eq or Hash on a struct with an f64 field: error[E0277]: the trait bound `f64: Eq` is not satisfied.
  • Printing a type with {} that only derives Debug: error[E0277]: `Config` doesn't implement `std::fmt::Display` — use {:?} or implement Display.
  • Implementing Into directly instead of From. It works but you lose the free reverse direction; the standard advice is to implement From.
  • Assuming derived Ord sorts by the field you care about. It uses the first field first. Reorder the fields or use sort_by_key.

Practice

Create #[derive(Debug, Clone, Copy, PartialEq)] struct Celsius(f64); and struct Fahrenheit(f64); with the same derives. Implement From<Celsius> for Fahrenheit (F = C × 9 / 5 + 32) and Display for both so they print like 100.0°C and 212.0°F. Convert 100 °C and -40 °C using .into().

Show solution
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq)]
struct Celsius(f64);

#[derive(Debug, Clone, Copy, PartialEq)]
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

impl fmt::Display for Fahrenheit {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°F", self.0)
    }
}

fn main() {
    for c in [Celsius(100.0), Celsius(-40.0)] {
        let f: Fahrenheit = c.into();
        println!("{c} = {f}");
    }
}
100.0°C = 212.0°F
-40.0°C = -40.0°F