Comparison traits let generic code compare values, sort collections and look up keys. Their signatures tell the compiler which operations are available. Their behavioral rules tell you what those operations must mean.
Which traits require which others?
A supertrait is a required trait bound. For example, Ord: Eq + PartialOrd
means a type implementing Ord must also implement Eq and PartialOrd.
Those bounds in turn provide access to equality operations.
| Trait | Required comparison trait | What it adds |
|---|---|---|
PartialEq<Rhs = Self> |
None | eq, used by ==, and a default ne for !=. |
Eq |
PartialEq<Self> |
The promise that every value equals itself. |
PartialOrd<Rhs = Self> |
PartialEq<Rhs> |
partial_cmp, which may report incomparable values. |
Ord |
Eq and PartialOrd<Self> |
cmp, which always returns an ordering. |
Hash |
None | Feeds a value's hash representation into a hasher. |
Hash has no equality supertrait, but hash-map keys need both Eq and
Hash. A function bounded by T: Ord can use ==, <, cmp and
partial_cmp without listing each bound separately.
These relationships do not add stored fields or implement the required traits for you. Derive the traits or supply their implementations.
Another example is Copy: Clone. A T: Copy bound also permits clone(),
while an expression such as *value can copy from &T. Copy allows
implicit duplication without a custom copying operation; it is not a promise
that copying a large value is cheap. A type implementing Drop cannot also
implement Copy.
Equality can be partial
For same-type comparisons, PartialEq requires symmetry and transitivity:
reversing an equality comparison preserves its result, and if a == b and
b == c, then a == c. != must be the negation of ==. Eq adds
reflexivity: every value must equal itself. It introduces no comparison
method of its own.
Floating-point NaN is a useful example. With ordinary float comparison,
NaN does not equal itself and is incomparable with other values. Consequently,
f64 implements PartialEq and PartialOrd, but not Eq or Ord.
Partial relations are useful beyond floating-point numbers too: a domain
may have values for which neither is less than, equal to or greater than
the other.
PartialEq can also compare different types when the relevant implementation
exists. For example, a String can compare equal to a string literal. Its
Rhs parameter permits multiple comparison implementations for one type.
Cross-type comparisons must also respect symmetry and transitivity wherever
the corresponding implementations exist. Eq concerns the same-type
equality relation, so it has no Rhs parameter.
Sorting floats needs an explicit policy
A slice's sort() requires Ord, so it cannot directly sort f64 values.
sort_by(f64::total_cmp) supplies a total ordering, including NaNs:
use std::cmp::Ordering;
fn main() {
let nan = f64::NAN;
assert_eq!(nan.partial_cmp(&1.0), None);
assert!(!nan.eq(&nan));
let mut values = [3.0_f64, -0.0, 0.0, 1.0];
values.sort_by(f64::total_cmp);
assert_eq!(values[0].to_bits(), (-0.0_f64).to_bits());
assert_eq!(values[1].to_bits(), 0.0_f64.to_bits());
assert_eq!(values[2..], [1.0, 3.0]);
assert_eq!(-0.0_f64, 0.0);
assert_eq!((-0.0_f64).total_cmp(&0.0), Ordering::Less);
}
total_cmp distinguishes negative and positive zero even though ordinary
float equality treats them as equal. It also orders NaNs by their sign and
representation; negative NaNs precede negative infinity and positive NaNs
follow positive infinity. It does not change f64's equality operators or
give f64 an Ord implementation.
If you wrap floats in your own ordered type, choose equality consistently
with the ordering. Combining ordinary float equality with total_cmp for
Ord would disagree on signed zeros and NaNs. Using
partial_cmp(...).unwrap() instead panics whenever it compares an
incomparable pair; only use that policy when your inputs exclude NaNs.
Keep all ordering operations consistent
For a type implementing Ord, partial_cmp(a, b) must equal
Some(cmp(a, b)). cmp must return Equal exactly when equality holds.
Exactly one of a < b, a == b or a > b must hold, and comparisons must
be transitive. Returning an Ordering value alone does not establish these
properties.
For an ordinary struct whose fields all participate in comparison, deriving the four traits is a useful default. Derived struct ordering compares fields in declaration order, stopping at the first difference. Reordering fields can therefore change the public ordering behavior.
If you need explicit comparison logic, keep it in one place and make the other operations agree. This complete example compares version numbers by major version, then minor version:
use std::cmp::Ordering;
#[derive(Debug)]
struct Version {
major: u32,
minor: u32,
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
(self.major, self.minor).cmp(&(other.major, other.minor))
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Version {
fn eq(&self, other: &Self) -> bool {
(self.major, self.minor) == (other.major, other.minor)
}
}
impl Eq for Version {}
fn main() {
let a = Version { major: 1, minor: 9 };
let b = Version { major: 2, minor: 0 };
assert!(a < b);
assert_eq!(a.partial_cmp(&b), Some(a.cmp(&b)));
assert_eq!(a, Version { major: 1, minor: 9 });
}
For a one-off sort by a particular field, sort_by_key may be simpler than
defining an ordering for the whole type. A broken ordering may cause a sort
to panic or return an unspecified order; a lack of panic is not validation.
catch_unwind only catches unwinding panics, not a process configured to
abort on panic.
Equal keys must hash alike
If a == b, hashing them with the same hashing setup must produce equal
hashes. The reverse is not required: unequal values may collide. A valid
hash function can therefore use fewer fields than equality, although this
may create more collisions.
Suppose accounts are equal by ID and ignore a changing timestamp. Hashing that timestamp would risk giving equal accounts different hashes. Such inconsistencies can break lookups; no specific failure or diagnostic is guaranteed. Deriving both equality and hashing uses the fields consistently, provided the field implementations honor their own contracts. Custom equality needs a corresponding review of hashing.
Often, a separate key type makes the intent clearer:
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Hash)]
struct AccountId(u64);
struct Account {
name: String,
last_seen: u64,
}
fn main() {
let mut accounts = HashMap::new();
accounts.insert(
AccountId(7),
Account { name: "Mira".into(), last_seen: 10 },
);
let account = accounts.get_mut(&AccountId(7)).expect("known account");
account.last_seen = 20;
assert_eq!(accounts[&AccountId(7)].name, "Mira");
assert_eq!(accounts[&AccountId(7)].last_seen, 20);
}
Here identity belongs to AccountId; the account's other fields can change
without changing the key. Do not change a stored key's equality or hash
behavior through interior mutability while it is in a hash map. Ordered-map
keys likewise need stable ordering while stored.
What the tools can check
The compiler checks required implementations and method signatures. It does
not prove equality, ordering or hashing laws. Clippy can identify suspicious
combinations, including derived_hash_with_manual_eq,
derive_ord_xor_partial_ord and non_canonical_partial_ord_impl. These
checks are useful, but passing them does not prove consistent behavior.
Derive only traits whose meaning fits your type and whose field bounds are
satisfied. Having no floating-point fields does not by itself establish that
a type supports Eq. Test custom implementations with equal values,
differing fields and boundary cases, including consistency between equality,
comparison and hashing.