Skip to content
← Back to explainers

Rust explainer

Equality, ordering and hashing: keeping traits consistent

About 6 min to read. Allow extra time to work through examples.

Learn what PartialEq, Eq, PartialOrd, Ord and Hash promise, how their bounds fit together, and how to choose consistent behavior for your own types.

This article uses Rust. You can make it your practice mode to browse related content across the site.

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.

step 7 of 24

Generics and Traits

Up next Supertraits and default methods