Full title: [Advanced Rust] 2.2. API Design Principles of Unsurprising Pt.2 - Implementing Clone, Default, PartialEq, PartialOrd, Hash, Eq, and Ord

2.2.1. It Is Recommended to Implement the Clone Trait and the Default Trait

Clone Trait

The Clone trait in Rust allows an implementer to explicitly create a deep copy of itself through the clone method, as opposed to the by-value copy provided by the Copy trait.

Example:

#[derive(Debug, Clone)]
struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(name: String, age: u32) -> Self {
        Self { name, age }
    }
}

fn main() {
    let person1 = Person::new("John".to_owned(), 25);
    let person2 = person1.clone();

    println!("{:?}", person1);
    println!("{:?}", person2);
}

Enter fullscreen mode Exit fullscreen mode

  • The Person struct implements the Clone trait
  • In main, person2 clones the data from person1 because it implements Clone

Output:

Person { name: "John", age: 25 }
Person { name: "John", age: 25 }

Enter fullscreen mode Exit fullscreen mode


Default Trait

The Default trait in Rust allows a type to define a default value and return that default instance through the default() method.

Example:

#[derive(Default)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point::default();

    println!("Point is at ({}, {})", p.x, p.y);
}

Enter fullscreen mode Exit fullscreen mode

Output:

Point is at (0, 0)

Enter fullscreen mode Exit fullscreen mode

2.2.2. It Is Recommended to Implement the PartialEq, PartialOrd, Hash, Eq, and Ord Traits

PartialEq Trait

PartialEq provides support for the == and != operators, allowing custom types to participate in partial equality comparisons.

Example:

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point1: Point = Point { x: 1, y: 2 };
    let point2: Point = Point { x: 1, y: 2 };
    let point3: Point = Point { x: 3, y: 4 };

    println!("point1 == point2: {}", point1 == point2);
    println!("point1 == point3: {}", point1 == point3);
}

Enter fullscreen mode Exit fullscreen mode

  • By implementing PartialEq, we can compare whether two structs are equal

Output:

point1 == point2: true
point1 == point3: false

Enter fullscreen mode Exit fullscreen mode


PartialOrd, Eq, and Ord Traits

PartialOrd provides support for the <, <=, >, and >= operators, allowing custom types to participate in partial ordering comparisons, where some values may not be comparable.

Eq is a stricter version of PartialEq that requires equality to be reflexive (a == a is always true). You must implement PartialEq before implementing Eq.

Ord is a stricter version of PartialOrd that requires a total ordering, enabling complete sorting logic for types such as BTreeMap and BTreeSet. You must implement PartialOrd before implementing Ord.

Example:

use std::collections::BTreeMap;

#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let mut ages = BTreeMap::new();

    let person1 = Person {
        name: String::from("Alice"),
        age: 25,
    };

    let person2 = Person {
        name: String::from("Bob"),
        age: 30,
    };

    let person3 = Person {
        name: String::from("Charlie"),
        age: 20,
    };

    ages.insert(person1.clone(), "Alice's Age");
    ages.insert(person2.clone(), "Bob's Age");
    ages.insert(person3.clone(), "Charlie's Age");

    for (person, description) in &ages {
        println!("{}: {} - {:?}", person.name, person.age, description);
    }
}

Enter fullscreen mode Exit fullscreen mode

  • BTreeMap stores entries in sorted order. Because it sorts by the key's value, the key type must implement Ord for total ordering and Eq for equality comparison.
    • Implementing Ord requires PartialOrd first
    • Implementing Eq requires PartialEq first

Hash Trait

Hash allows a type to implement the hash() method, which supports hash-based collections such as HashMap and HashSet. The Hash trait itself does not require Eq as a supertrait, but hash collections require K: Eq + Hash, and equal values must produce the same hash. So in practice, you should implement PartialEq and Eq together with Hash whenever the type will be used as a hash-map/set key.

Example:

use std::collections::HashSet;
use std::hash::{Hash, Hasher};

#[derive(Debug, PartialEq, Eq, Clone)]
struct Person {
    name: String,
    age: u32,
}

impl Hash for Person {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.name.hash(state);
        self.age.hash(state);
    }
}

fn main() {
    let mut persons = HashSet::new();

    let person1 = Person {
        name: "Alice".to_string(),
        age: 25,
    };

    let person2 = Person {
        name: "Bob".to_string(),
        age: 30,
    };

    let person3 = Person {
        name: "Charlie".to_string(),
        age: 20,
    };

    persons.insert(person1.clone());
    persons.insert(person2.clone());
    persons.insert(person3.clone());

    println!("Persons: {:#?}", persons);
}

Enter fullscreen mode Exit fullscreen mode

  • HashSet is used to store a set of unique elements, while HashMap is used to store key-value pairs.

Output:

Persons: {
    Person {
        name: "Bob",
        age: 30,
    },
    Person {
        name: "Charlie",
        age: 20,
    },
    Person {
        name: "Alice",
        age: 25,
    },
}

Enter fullscreen mode Exit fullscreen mode


Eq and PartialEq, Ord and PartialOrd

Eq has additional semantic requirements compared with PartialEq, and Ord has additional semantic requirements compared with PartialOrd. You should only implement them when those semantics apply to your type.

The additional semantics of Eq compared with PartialEq are:

  • Reflexivity: for all a, a == a must always hold

(PartialEq already expects symmetry and transitivity; Eq is the marker that equality is also reflexive, so there are no “partial” equalities such as f32::NAN != f32::NAN.)

The additional semantics of Ord compared with PartialOrd are:

  • Totality / comparability: for all a and b, exactly one of a < b, a == b, or a > b holds (equivalently, partial_cmp never returns None)
  • Reflexivity: for all a, a <= a and a >= a must always hold
  • Antisymmetry: if a <= b and b <= a, then a == b must hold
  • Transitivity: if a <= b and b <= c, then a <= c must hold