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
Personstruct implements theClonetrait - In
main,person2clones the data fromperson1because it implementsClone
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
-
BTreeMapstores entries in sorted order. Because it sorts by the key's value, the key type must implementOrdfor total ordering andEqfor equality comparison.- Implementing
OrdrequiresPartialOrdfirst - Implementing
EqrequiresPartialEqfirst
- Implementing
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
-
HashSetis used to store a set of unique elements, whileHashMapis 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 == amust 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
aandb, exactly one ofa < b,a == b, ora > bholds (equivalently,partial_cmpnever returnsNone) -
Reflexivity: for all
a,a <= aanda >= amust always hold -
Antisymmetry: if
a <= bandb <= a, thena == bmust hold -
Transitivity: if
a <= bandb <= c, thena <= cmust hold
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.