Eq, PartialEq, Ord e PartialOrd em Rust

Guia completo sobre traits de igualdade e ordenação em Rust: PartialEq, Eq, PartialOrd e Ord com exemplos de implementação.

O que são os Traits de Igualdade e Ordenação?

Rust define quatro traits para comparação de valores, organizados em uma hierarquia:

  • PartialEq: igualdade parcial — permite == e !=. “Parcial” porque nem todo valor precisa ser igual a si mesmo (como NaN em ponto flutuante).
  • Eq: igualdade total — um marker trait que estende PartialEq, garantindo que a == a é sempre true (reflexividade).
  • PartialOrd: ordenação parcial — permite <, >, <=, >=. Retorna Option<Ordering> porque nem todos os pares de valores são comparáveis.
  • Ord: ordenação total — estende PartialOrd e Eq, garantindo que qualquer par de valores pode ser comparado. Retorna Ordering diretamente.

A hierarquia é: Eq: PartialEq e Ord: Eq + PartialOrd.


Definição dos Traits

// std::cmp::PartialEq
pub trait PartialEq<Rhs = Self>
where
    Rhs: ?Sized,
{
    fn eq(&self, other: &Rhs) -> bool;

    // Implementação padrão: !self.eq(other)
    fn ne(&self, other: &Rhs) -> bool { ... }
}

// std::cmp::Eq (marker trait)
pub trait Eq: PartialEq { }

// std::cmp::PartialOrd
pub trait PartialOrd<Rhs = Self>: PartialEq<Rhs>
where
    Rhs: ?Sized,
{
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;

    fn lt(&self, other: &Rhs) -> bool { ... }
    fn le(&self, other: &Rhs) -> bool { ... }
    fn gt(&self, other: &Rhs) -> bool { ... }
    fn ge(&self, other: &Rhs) -> bool { ... }
}

// std::cmp::Ord
pub trait Ord: Eq + PartialOrd {
    fn cmp(&self, other: &Self) -> Ordering;

    fn max(self, other: Self) -> Self { ... }
    fn min(self, other: Self) -> Self { ... }
    fn clamp(self, min: Self, max: Self) -> Self { ... }
}

// O enum Ordering
pub enum Ordering {
    Less,
    Equal,
    Greater,
}

Como Implementar: derive vs impl manual

Derive de todos os quatro

Para tipos cujos campos já implementam os traits:

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Versao {
    maior: u32,
    menor: u32,
    patch: u32,
}

fn main() {
    let v1 = Versao { maior: 1, menor: 2, patch: 3 };
    let v2 = Versao { maior: 1, menor: 3, patch: 0 };
    let v3 = Versao { maior: 1, menor: 2, patch: 3 };

    // PartialEq: == e !=
    assert!(v1 == v3);
    assert!(v1 != v2);

    // PartialOrd/Ord: <, >, <=, >=
    assert!(v1 < v2);   // 1.2.3 < 1.3.0

    // Ord permite ordenação
    let mut versoes = vec![v2, v1, v3];
    versoes.sort();  // Requer Ord
    println!("{:?}", versoes);
    // [Versao { maior: 1, menor: 2, patch: 3 }, ...]
}

Quando derivado, a comparação segue a ordem de declaração dos campos — o primeiro campo tem maior prioridade.

Implementação manual de PartialEq

#[derive(Debug)]
struct Email {
    endereco: String,
}

// Compara emails ignorando maiúsculas/minúsculas
impl PartialEq for Email {
    fn eq(&self, other: &Self) -> bool {
        self.endereco.to_lowercase() == other.endereco.to_lowercase()
    }
}

impl Eq for Email {}  // Garante reflexividade

fn main() {
    let e1 = Email { endereco: String::from("Ana@Exemplo.com") };
    let e2 = Email { endereco: String::from("ana@exemplo.com") };

    assert!(e1 == e2); // true! Case-insensitive
    println!("Emails iguais: {}", e1 == e2);
}

Implementação manual de Ord

use std::cmp::Ordering;

#[derive(Debug, Eq, PartialEq)]
struct Tarefa {
    prioridade: u8,    // 1 = mais urgente
    descricao: String,
}

// Ordena por prioridade (menor número = mais urgente = "maior")
impl Ord for Tarefa {
    fn cmp(&self, other: &Self) -> Ordering {
        // Inverte a comparação de prioridade
        other.prioridade.cmp(&self.prioridade)
            .then_with(|| self.descricao.cmp(&other.descricao))
    }
}

impl PartialOrd for Tarefa {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

fn main() {
    let mut tarefas = vec![
        Tarefa { prioridade: 3, descricao: String::from("Documentação") },
        Tarefa { prioridade: 1, descricao: String::from("Bug crítico") },
        Tarefa { prioridade: 2, descricao: String::from("Nova feature") },
        Tarefa { prioridade: 1, descricao: String::from("Atualizar deps") },
    ];

    tarefas.sort();

    for t in &tarefas {
        println!("[P{}] {}", t.prioridade, t.descricao);
    }
    // [P1] Atualizar deps
    // [P1] Bug crítico
    // [P2] Nova feature
    // [P3] Documentação
}

Exemplos Práticos

Exemplo 1: Por que f64 é apenas PartialOrd

Números de ponto flutuante possuem um valor especial, NaN (Not a Number), que não é igual a si mesmo e não é comparável com nenhum outro valor:

fn main() {
    let x = f64::NAN;

    // NaN não é igual a si mesmo!
    assert!(x != x);
    println!("NaN == NaN? {}", x == x); // false

    // NaN não é menor, maior, nem igual a nada
    println!("NaN < 0.0? {}", x < 0.0);   // false
    println!("NaN > 0.0? {}", x > 0.0);   // false
    println!("NaN == 0.0? {}", x == 0.0);  // false

    // partial_cmp retorna None para NaN
    println!("{:?}", x.partial_cmp(&0.0)); // None

    // Por isso f64 não implementa Eq nem Ord
    // Isso significa que Vec<f64> não pode usar .sort()
    let mut numeros = vec![3.0, 1.0, 2.0];
    // numeros.sort();  // ERRO: f64 não implementa Ord

    // Use sort_by com partial_cmp ou total_cmp
    numeros.sort_by(|a, b| a.partial_cmp(b).unwrap());
    println!("{:?}", numeros); // [1.0, 2.0, 3.0]

    // Ou use total_cmp (disponível desde Rust 1.62)
    numeros.sort_by(f64::total_cmp);
    println!("{:?}", numeros);
}

Exemplo 2: PartialEq entre tipos diferentes

#[derive(Debug)]
struct Metros(f64);

#[derive(Debug)]
struct Centimetros(f64);

impl PartialEq<Centimetros> for Metros {
    fn eq(&self, other: &Centimetros) -> bool {
        (self.0 * 100.0 - other.0).abs() < f64::EPSILON
    }
}

impl PartialEq<Metros> for Centimetros {
    fn eq(&self, other: &Metros) -> bool {
        (self.0 - other.0 * 100.0).abs() < f64::EPSILON
    }
}

fn main() {
    let m = Metros(1.5);
    let cm = Centimetros(150.0);

    println!("1.5m == 150cm? {}", m == cm); // true
    println!("150cm == 1.5m? {}", cm == m); // true
}

Exemplo 3: Ordenação customizada com sort_by

#[derive(Debug)]
struct Produto {
    nome: String,
    preco: f64,
    avaliacao: f64,
}

fn main() {
    let mut produtos = vec![
        Produto { nome: "Mouse".into(), preco: 89.90, avaliacao: 4.5 },
        Produto { nome: "Teclado".into(), preco: 199.90, avaliacao: 4.8 },
        Produto { nome: "Monitor".into(), preco: 1299.90, avaliacao: 4.2 },
        Produto { nome: "Webcam".into(), preco: 199.90, avaliacao: 3.9 },
    ];

    // Ordenar por preço (crescente)
    produtos.sort_by(|a, b| {
        a.preco.partial_cmp(&b.preco).unwrap()
    });

    println!("Por preço:");
    for p in &produtos {
        println!("  {} - R${:.2}", p.nome, p.preco);
    }

    // Ordenar por avaliação (decrescente)
    produtos.sort_by(|a, b| {
        b.avaliacao.partial_cmp(&a.avaliacao).unwrap()
    });

    println!("\nPor avaliação:");
    for p in &produtos {
        println!("  {} - {:.1} estrelas", p.nome, p.avaliacao);
    }
}

Exemplo 4: Ord com BinaryHeap

use std::collections::BinaryHeap;
use std::cmp::Ordering;

#[derive(Debug, Eq, PartialEq)]
struct Evento {
    timestamp: u64,
    descricao: String,
}

// BinaryHeap é max-heap. Invertemos para processar o mais antigo primeiro.
impl Ord for Evento {
    fn cmp(&self, other: &Self) -> Ordering {
        other.timestamp.cmp(&self.timestamp) // Invertido!
    }
}

impl PartialOrd for Evento {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

fn main() {
    let mut fila = BinaryHeap::new();

    fila.push(Evento { timestamp: 300, descricao: "Logout".into() });
    fila.push(Evento { timestamp: 100, descricao: "Login".into() });
    fila.push(Evento { timestamp: 200, descricao: "Compra".into() });

    // Processa na ordem cronológica (timestamp mais baixo primeiro)
    while let Some(evento) = fila.pop() {
        println!("[{}] {}", evento.timestamp, evento.descricao);
    }
    // [100] Login
    // [200] Compra
    // [300] Logout
}

Exemplo 5: Combinando Eq com HashMap

use std::collections::HashMap;

#[derive(Debug, Hash, PartialEq, Eq)]
struct ChaveComposta {
    categoria: String,
    id: u32,
}

fn main() {
    let mut inventario: HashMap<ChaveComposta, u32> = HashMap::new();

    let chave1 = ChaveComposta {
        categoria: String::from("eletrônicos"),
        id: 1,
    };

    let chave2 = ChaveComposta {
        categoria: String::from("eletrônicos"),
        id: 2,
    };

    inventario.insert(chave1, 50);
    inventario.insert(chave2, 30);

    let busca = ChaveComposta {
        categoria: String::from("eletrônicos"),
        id: 1,
    };

    // HashMap requer Eq + Hash para suas chaves
    if let Some(qtd) = inventario.get(&busca) {
        println!("Quantidade: {}", qtd); // 50
    }
}

Padrões e Boas Práticas

  1. Derive quando possível: #[derive(PartialEq, Eq, PartialOrd, Ord)] é suficiente para a maioria dos tipos. A ordem de comparação segue a ordem dos campos.

  2. Consistência entre traits: Se você implementa Ord, sua implementação de PartialOrd deve ser consistente. O padrão é partial_cmp retornar Some(self.cmp(other)).

  3. Eq exige reflexividade: Só implemente Eq se a == a é sempre verdadeiro. Por isso, f64 implementa PartialEq mas não Eq.

  4. Hash e Eq devem ser consistentes: Se a == b, então hash(a) == hash(b). Veja Hash Trait para detalhes.

  5. Use then_with para ordenação composta: Ao implementar Ord, encadeie comparações com .then_with(|| ...) para critérios secundários.

  6. sort_by para tipos sem Ord: Quando o tipo não implementa Ord (como structs com f64), use .sort_by() com uma closure.

  7. Cuidado com PartialEq manual e HashMap: Se você personaliza PartialEq (por exemplo, case-insensitive), certifique-se de que Hash é igualmente personalizado.


Veja Também