Enums (enumerações) são um dos pilares da programação em Rust, permitindo definir um tipo que pode ser um de vários valores possíveis. Diferente de linguagens como C ou Java, os enums em Rust são extremamente poderosos: cada variante pode carregar dados de tipos diferentes, e o compilador garante que todos os casos sejam tratados (com match). Esta aula aborda desde a definição básica até padrões avançados de uso, incluindo o famoso Option.

Enums são essenciais para escrever código seguro e expressivo. Eles permitem modelar domínios complexos de forma tipada, eliminando a necessidade de sentinelas ou flags mágicas. Vamos começar com a sintaxe fundamental.

Definindo enums

Um enum é declarado com a palavra-chave enum, seguida do nome e um bloco com as variantes. Cada variante é um identificador (em CamelCase) e pode ou não conter dados associados. Exemplo simples:

enum DiaDaSemana {
    Segunda,
    Terca,
    Quarta,
    Quinta,
    Sexta,
    Sabado,
    Domingo,
}

Para usar um valor do enum, basta referenciar a variante com a sintaxe de path: DiaDaSemana::Segunda. O compilador trata cada variante como um valor distinto do tipo DiaDaSemana. Enums sem dados são semanticamente similares a constantes, mas são tipos próprios.

Enums também podem ser usados com a diretiva #[derive(...)] para obter traits como Debug, Clone, Copy, PartialEq, etc. Exemplo:

#[derive(Debug, PartialEq)]
enum Cor {
    Vermelho,
    Verde,
    Azul,
}

fn main() {
    let c = Cor::Verde;
    println!("{:?}", c); // Verde
    assert_eq!(c, Cor::Verde);
}

Enums são tipos de tamanho fixo: o compilador determina o tamanho necessário para armazenar a maior variante (mais um discriminante). Isso torna enums eficientes e previsíveis.

Variantes com dados

O verdadeiro poder dos enums em Rust aparece quando associamos dados a cada variante. Cada variante pode carregar uma tupla, um struct, ou até mesmo ser um struct anônimo. Exemplo clássico:

enum Mensagem {
    Sair,
    Mover { x: i32, y: i32 },
    Escrever(String),
    MudarCor(u8, u8, u8),
}

Aqui, Sair não tem dados; Mover tem dois campos nomeados (struct-like); Escrever carrega uma String; MudarCor carrega três u8 (tuple-like). Isso permite modelar qualquer tipo de dado heterogêneo de forma segura.

Para acessar os dados, usamos match ou if let. Exemplo com match:

fn processar(msg: Mensagem) {
    match msg {
        Mensagem::Sair => println!("Encerrando..."),
        Mensagem::Mover { x, y } => println!("Move para ({}, {})", x, y),
        Mensagem::Escrever(texto) => println!("Texto: {}", texto),
        Mensagem::MudarCor(r, g, b) => println!("Cor: RGB({}, {}, {})", r, g, b),
    }
}

O match deve ser exaustivo: o compilador obriga a cobrir todas as variantes. Se você quiser ignorar algumas, use _. O if let é útil para lidar com apenas uma variante:

if let Mensagem::Escrever(texto) = msg {
    println!("Mensagem: {}", texto);
}

Variantes com dados permitem representar árvores, nós de AST, comandos, etc., de forma elegante e segura.

Option como enum

O enum Option<T> é definido na biblioteca padrão como:

enum Option<T> {
    None,
    Some(T),
}

Ele substitui o uso de null ou ponteiros nulos, forçando o programador a tratar a ausência de valor explicitamente. None indica ausência; Some(T) carrega um valor do tipo T. Isso elimina uma classe inteira de bugs (null pointer exceptions).

Exemplo de uso:

fn dividir(numerador: f64, denominador: f64) -> Option<f64> {
    if denominador == 0.0 {
        None
    } else {
        Some(numerador / denominador)
    }
}

fn main() {
    let resultado = dividir(10.0, 2.0);
    match resultado {
        Some(valor) => println!("Resultado: {}", valor),
        None => println!("Divisão por zero!"),
    }
}

Option possui diversos métodos utilitários, como unwrap, expect, map, and_then, unwrap_or, etc., que facilitam o encadeamento de operações. Exemplo com map:

let x: Option<i32> = Some(5);
let y = x.map(|v| v * 2); // Some(10)

Option é onipresente em Rust: operações de busca em coleções, parsing, I/O, etc., retornam Option. O compilador garante que você nunca esqueça de verificar se um valor existe.

Casos de uso

Enums são usados para modelar estados finitos, como máquinas de estado:

enum Estado {
    Ativo,
    Inativo,
    Suspenso { motivo: String },
}

Em jogos, para representar itens:

enum Item {
    Arma { dano: i32 },
    Pocao { cura: i32 },
    Armadura { defesa: i32 },
}

Em processamento de dados, para representar diferentes tipos de nós em uma árvore:

enum No {
    Folha(i32),
    Interno(Box<No>, Box<No>),
}

Enums também são a base para tratamento de erros com Result<T, E>, que é outro enum (Ok(T) e Err(E)). Combinado com match, permite escrever código robusto e explícito.

Boas práticas: prefira enums a booleanos para representar estados binários (ex: enum Status { Ativo, Inativo } em vez de bool ativo). Use #[non_exhaustive] em enums públicos de bibliotecas para permitir adicionar variantes no futuro sem quebrar código cliente.

Referências

Exercícios

  1. Crie um enum chamado Forma com variantes Circulo(f64) (raio), Retangulo { largura: f64, altura: f64 } e Triangulo { base: f64, altura: f64 }. Escreva uma função que recebe uma Forma e retorna a área (use PI = 3.14159).

    ✓ Resposta:
    enum Forma {
        Circulo(f64),
        Retangulo { largura: f64, altura: f64 },
        Triangulo { base: f64, altura: f64 },
    }
    
    fn area(forma: Forma) -> f64 {
        match forma {
            Forma::Circulo(raio) => 3.14159 * raio * raio,
            Forma::Retangulo { largura, altura } => largura * altura,
            Forma::Triangulo { base, altura } => (base * altura) / 2.0,
        }
    }
    
    fn main() {
        let c = Forma::Circulo(10.0);
        println!("Área do círculo: {}", area(c));
        let r = Forma::Retangulo { largura: 5.0, altura: 7.0 };
        println!("Área do retângulo: {}", area(r));
        let t = Forma::Triangulo { base: 6.0, altura: 4.0 };
        println!("Área do triângulo: {}", area(t));
    }
  2. Implemente uma função que recebe um Option<i32> e retorna o dobro do valor se for Some, ou 0 se for None. Use match.

    ✓ Resposta:
    fn dobro(valor: Option<i32>) -> i32 {
        match valor {
            Some(x) => x * 2,
            None => 0,
        }
    }
    
    fn main() {
        assert_eq!(dobro(Some(5)), 10);
        assert_eq!(dobro(None), 0);
    }
  3. Crie um enum StatusPedido com variantes Pendente, Processando, Enviado e Entregue. Escreva uma função que imprime uma mensagem diferente para cada estado.

    ✓ Resposta:
    enum StatusPedido {
        Pendente,
        Processando,
        Enviado,
        Entregue,
    }
    
    fn exibir_status(status: StatusPedido) {
        match status {
            StatusPedido::Pendente => println!("Pedido aguardando pagamento."),
            StatusPedido::Processando => println!("Pedido está sendo preparado."),
            StatusPedido::Enviado => println!("Pedido saiu para entrega."),
            StatusPedido::Entregue => println!("Pedido entregue com sucesso!"),
        }
    }
    
    fn main() {
        exibir_status(StatusPedido::Pendente);
        exibir_status(StatusPedido::Entregue);
    }
  4. Escreva uma função que recebe uma lista de Option<i32> e retorna a soma de todos os valores Some. Ignore os None.

    ✓ Resposta:
    fn soma_some(lista: Vec<Option<i32>>) -> i32 {
        let mut soma = 0;
        for item in lista {
            if let Some(valor) = item {
                soma += valor;
            }
        }
        soma
    }
    
    fn main() {
        let dados = vec![Some(10), None, Some(20), None, Some(30)];
        assert_eq!(soma_some(dados), 60);
    }
  5. Defina um enum Resultado<T, E> com variantes Sucesso(T) e Erro(E). Implemente uma função que simula uma operação de divisão que retorna Resultado<f64, String> (erro se denominador zero).

    ✓ Resposta:
    enum Resultado<T, E> {
        Sucesso(T),
        Erro(E),
    }
    
    fn dividir(numerador: f64, denominador: f64) -> Resultado<f64, String> {
        if denominador == 0.0 {
            Resultado::Erro(String::from("Divisão por zero"))
        } else {
            Resultado::Sucesso(numerador / denominador)
        }
    }
    
    fn main() {
        match dividir(10.0, 2.0) {
            Resultado::Sucesso(v) => println!("Resultado: {}", v),
            Resultado::Erro(e) => println!("Erro: {}", e),
        }
        match dividir(10.0, 0.0) {
            Resultado::Sucesso(v) => println!("Resultado: {}", v),
            Resultado::Erro(e) => println!("Erro: {}", e),
        }
    }