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 (comoNaNem ponto flutuante).Eq: igualdade total — um marker trait que estendePartialEq, garantindo quea == aé sempretrue(reflexividade).PartialOrd: ordenação parcial — permite<,>,<=,>=. RetornaOption<Ordering>porque nem todos os pares de valores são comparáveis.Ord: ordenação total — estendePartialOrdeEq, garantindo que qualquer par de valores pode ser comparado. RetornaOrderingdiretamente.
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
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.Consistência entre traits: Se você implementa
Ord, sua implementação dePartialOrddeve ser consistente. O padrão épartial_cmpretornarSome(self.cmp(other)).Eq exige reflexividade: Só implemente
Eqsea == aé sempre verdadeiro. Por isso,f64implementaPartialEqmas nãoEq.Hash e Eq devem ser consistentes: Se
a == b, entãohash(a) == hash(b). Veja Hash Trait para detalhes.Use
then_withpara ordenação composta: Ao implementarOrd, encadeie comparações com.then_with(|| ...)para critérios secundários.sort_bypara tipos sem Ord: Quando o tipo não implementaOrd(como structs comf64), use.sort_by()com uma closure.Cuidado com PartialEq manual e HashMap: Se você personaliza
PartialEq(por exemplo, case-insensitive), certifique-se de queHashé igualmente personalizado.
Veja Também
- Hash Trait — contrato Hash+Eq para HashMap e HashSet
- Clone e Copy — frequentemente derivados junto com Eq
- Default Trait — outro trait do conjunto padrão de derives
- Display e Debug — complementa o grupo de derives comuns
- Ordenar Vetor — receitas práticas de ordenação