Nesta aula, vamos explorar os traits mais comuns da biblioteca padrão do Rust. Esses traits fornecem comportamentos fundamentais como impressão, clonagem, comparação e ordenação. Entender como e quando implementá-los é essencial para escrever código idiomático e reutilizável.

Traits em Rust são como interfaces em outras linguagens: definem um conjunto de métodos que um tipo deve implementar. A std oferece vários traits que são tão onipresentes que o compilador pode derivá-los automaticamente usando #[derive]. Vamos ver cada um deles em detalhes.

Debug, Clone, Copy

Debug é o trait usado para formatação de depuração. Ele é ativado com {:?} na macro println! ou format!. Implementá-lo permite imprimir o conteúdo de uma struct ou enum de forma legível para depuração. Quase todas as estruturas de dados devem implementar Debug.

Clone permite criar uma cópia explícita de um valor. Ao contrário de Copy, que é implícito, Clone exige uma chamada ao método .clone(). Tipos que alocam recursos (como String, Vec) implementam Clone, mas não Copy, porque uma cópia implícita poderia causar vazamentos ou dupla liberação.

Copy é um marcador que indica que o tipo pode ser copiado simplesmente com uma cópia de bits (como inteiros, booleanos, floats). Tipos que implementam Copy devem implementar Clone também. Copy é implícito: ao atribuir ou passar por valor, o original ainda pode ser usado. Apenas tipos que estão na pilha (sem heap) podem ser Copy.

#[derive(Debug, Clone, Copy)]
struct Ponto {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Ponto { x: 10, y: 20 };
    let p2 = p1; // Copy: p1 ainda válido
    println!("{:?}", p1); // Debug
    let p3 = p1.clone(); // Clone explícito
    println!("{:?}", p3);
}

PartialEq, Eq, Ord

PartialEq define a igualdade (== e !=). Ele permite que alguns valores não sejam comparáveis (ex.: f64 com NaN). Eq é um subtrait de PartialEq que garante reflexividade total: todo valor é igual a si mesmo. Tipos inteiros e strings implementam Eq; floats implementam apenas PartialEq devido ao NaN.

Ord define uma ordenação total (<, >, <=, >=). Requer que o tipo também implemente Eq e PartialOrd. Com Ord, podemos usar .sort() e BinaryHeap. Para tipos que não têm uma ordem natural, implemente manualmente.

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Pessoa {
    nome: String,
    idade: u8,
}

fn main() {
    let mut pessoas = vec![
        Pessoa { nome: "Ana".into(), idade: 30 },
        Pessoa { nome: "Bia".into(), idade: 25 },
    ];
    pessoas.sort(); // Ordena por nome depois idade (ordem lexicográfica)
    println!("{:?}", pessoas);
}

Display

Display é usado para formatação amigável ao usuário, com {}. Diferente de Debug, que é para depuração, Display deve produzir uma saída legível para o usuário final. Não pode ser derivado automaticamente com derive; você precisa implementá-lo manualmente.

use std::fmt;

struct Temperatura {
    celsius: f64,
}

impl fmt::Display for Temperatura {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}°C", self.celsius)
    }
}

fn main() {
    let t = Temperatura { celsius: 25.5 };
    println!("{}", t); // 25.5°C
}

derive

A macro #[derive] gera implementações automáticas de traits para structs e enums. É uma ferramenta poderosa que reduz código repetitivo. Os traits deriváveis comuns são: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default. Para usar, basta adicionar #[derive(Trait1, Trait2, ...)] antes da definição do tipo.

Nem todos os traits podem ser derivados. Por exemplo, Display e From exigem implementação manual porque dependem de lógica específica do tipo. Além disso, a derivação só funciona se todos os campos do tipo implementarem o trait. Caso contrário, você receberá um erro de compilação.

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Livro {
    titulo: String,
    autor: String,
    ano: u16,
}

fn main() {
    let livro = Livro {
        titulo: "O Senhor dos Anéis".into(),
        autor: "J.R.R. Tolkien".into(),
        ano: 1954,
    };
    println!("{:?}", livro);
    let outro = livro.clone();
    println!("{}", livro == outro); // true
}

Boas práticas

  • Sempre implemente Debug para seus tipos, mesmo que não planeje usá-lo imediatamente. É útil para depuração e para usar macros como assert_eq!.
  • Use Clone quando precisar de cópias explícitas, mas evite clonar grandes estruturas desnecessariamente.
  • Implemente Copy apenas para tipos pequenos e que estejam inteiramente na pilha. Use Clone para tipos que alocam heap.
  • Para comparações, implemente PartialEq e Eq sempre que fizer sentido. Se seu tipo tem uma ordem natural, implemente Ord.
  • Não derive Ord se a ordem padrão (lexicográfica) não for a desejada. Nesse caso, implemente manualmente.
  • Prefira derive sempre que possível, mas não hesite em implementar manualmente quando a lógica padrão não atender.

Referências

Exercícios

  1. Crie uma struct Cor com campos r, g, b do tipo u8. Derive os traits Debug, Clone, Copy, PartialEq e Eq. Escreva um programa que testa a igualdade de duas cores e imprime uma delas com Debug.

    ✓ Resposta:
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    struct Cor {
        r: u8,
        g: u8,
        b: u8,
    }
    
    fn main() {
        let c1 = Cor { r: 255, g: 0, b: 0 };
        let c2 = Cor { r: 255, g: 0, b: 0 };
        println!("{:?}", c1);
        println!("{}", c1 == c2);
    }
  2. Implemente manualmente o trait Display para a struct Cor do exercício anterior, exibindo a cor no formato RGB(r, g, b). Teste com println!("{}", cor).

    ✓ Resposta:
    use std::fmt;
    
    impl fmt::Display for Cor {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "RGB({}, {}, {})", self.r, self.g, self.b)
        }
    }
    
    fn main() {
        let cor = Cor { r: 128, g: 128, b: 0 };
        println!("{}", cor);
    }
  3. Crie um enum Status com variantes Ativo e Inativo. Derive Debug, Clone, Copy, PartialEq e Eq. Escreva uma função que aceita Status por valor e imprime se é Ativo ou Inativo.

    ✓ Resposta:
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    enum Status {
        Ativo,
        Inativo,
    }
    
    fn verificar_status(s: Status) {
        if s == Status::Ativo {
            println!("Ativo");
        } else {
            println!("Inativo");
        }
    }
    
    fn main() {
        let s = Status::Ativo;
        verificar_status(s);
        println!("{:?}", s); // ainda válido por causa de Copy
    }
  4. Derive Ord para a struct Pessoa (nome: String, idade: u8). Crie um vetor de pessoas e ordene-o. A ordem padrão será por nome depois idade. Verifique com println!.

    ✓ Resposta:
    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
    struct Pessoa {
        nome: String,
        idade: u8,
    }
    
    fn main() {
        let mut pessoas = vec![
            Pessoa { nome: "Zeca".into(), idade: 20 },
            Pessoa { nome: "Ana".into(), idade: 30 },
            Pessoa { nome: "Ana".into(), idade: 25 },
        ];
        pessoas.sort();
        println!("{:?}", pessoas);
    }
  5. Implemente manualmente o trait PartialEq para uma struct Medida que contém um campo valor: f64. Duas medidas são iguais se seus valores são iguais (considere que NaN não é igual a nada). Teste com valores normais e com NaN.

    ✓ Resposta:
    struct Medida {
        valor: f64,
    }
    
    impl PartialEq for Medida {
        fn eq(&self, other: &Self) -> bool {
            self.valor == other.valor
        }
    }
    
    fn main() {
        let a = Medida { valor: 3.14 };
        let b = Medida { valor: 3.14 };
        let c = Medida { valor: f64::NAN };
        println!("{}", a == b); // true
        println!("{}", c == c); // false
    }