Rust

Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu Já leu

9 min de leitura

Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu
  Se structs são a forma de Rust agrupar dados relacionados, enums são a forma de expressar que um valor pode ser uma coisa ou outra. E

 

Se structs são a forma de Rust agrupar dados relacionados, enums são a forma de expressar que um valor pode ser uma coisa ou outra. Em linguagens como Java ou C, enums são basicamente constantes nomeadas. Em Rust, enums são muito mais poderosos — cada variante pode carregar dados diferentes, e o compilador exige que você trate todos os casos possíveis. Combinados com pattern matching, formam um dos sistemas mais expressivos da linguagem.

Este artigo vai mudar a forma como você pensa sobre modelagem de dados.


Enums básicos

A forma mais simples de enum define um conjunto de variantes nomeadas:

enum DirecaoCardinal {
    Norte,
    Sul,
    Leste,
    Oeste,
}

fn descricao(d: DirecaoCardinal) -> &'static str {
    match d {
        DirecaoCardinal::Norte => "Indo para o Norte",
        DirecaoCardinal::Sul   => "Indo para o Sul",
        DirecaoCardinal::Leste => "Indo para o Leste",
        DirecaoCardinal::Oeste => "Indo para o Oeste",
    }
}

fn main() {
    let direcao = DirecaoCardinal::Norte;
    println!("{}", descricao(direcao));
}

Note o match — voltaremos a ele em detalhes daqui a pouco. Por ora, perceba que ele é exaustivo: se você remover um dos braços, o compilador reclama. Nenhuma variante pode ser ignorada silenciosamente.


Enums com dados — onde a mágica começa

Em Rust, cada variante de um enum pode carregar dados — e cada variante pode carregar dados de tipos diferentes:

enum Forma {
    Circulo(f64),               // raio
    Retangulo(f64, f64),        // largura, altura
    Triangulo(f64, f64, f64),   // lados a, b, c
}

fn area(forma: &Forma) -> f64 {
    match forma {
        Forma::Circulo(raio) => std::f64::consts::PI * raio * raio,
        Forma::Retangulo(l, a) => l * a,
        Forma::Triangulo(a, b, c) => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}

fn main() {
    let formas = vec![
        Forma::Circulo(3.0),
        Forma::Retangulo(4.0, 5.0),
        Forma::Triangulo(3.0, 4.0, 5.0),
    ];

    for forma in &formas {
        println!("Área: {:.2}", area(forma));
    }
}

Saída:

Área: 28.27
Área: 20.00
Área: 6.00

Cada variante é essencialmente um tipo diferente embutido num mesmo enum. Isso é o que linguagens funcionais chamam de tipos soma — e é uma das ferramentas mais poderosas para modelar domínios com precisão.


Variantes com campos nomeados

Variantes de enum também podem ter campos nomeados, como structs:

enum Evento {
    Clique { x: i32, y: i32 },
    Teclado { tecla: char, modificador: bool },
    Redimensionar { largura: u32, altura: u32 },
    Fechar,
}

fn processar(evento: Evento) {
    match evento {
        Evento::Clique { x, y } => {
            println!("Clique em ({x}, {y})");
        }
        Evento::Teclado { tecla, modificador } => {
            let mod_str = if modificador { "Ctrl+" } else { "" };
            println!("Tecla: {mod_str}{tecla}");
        }
        Evento::Redimensionar { largura, altura } => {
            println!("Janela: {largura}×{altura}");
        }
        Evento::Fechar => {
            println!("Encerrando aplicação.");
        }
    }
}

fn main() {
    processar(Evento::Clique { x: 100, y: 200 });
    processar(Evento::Teclado { tecla: 's', modificador: true });
    processar(Evento::Redimensionar { largura: 1920, altura: 1080 });
    processar(Evento::Fechar);
}

Saída:

Clique em (100, 200)
Tecla: Ctrl+s
Janela: 1920×1080
Encerrando aplicação.

Um único tipo Evento representa quatro realidades completamente diferentes, cada uma com seus próprios dados. Sem herança, sem casting, sem verificações de tipo em tempo de execução. O compilador sabe exatamente o que cada variante carrega.


Option<T> — o fim do null

Tony Hoare, criador do null, chamou sua invenção de "billion dollar mistake" — em referência ao custo acumulado de bugs causados por null pointer exceptions ao longo das décadas. Rust não tem null. Em vez disso, tem Option<T>:

enum Option<T> {
    Some(T),  // existe um valor do tipo T
    None,     // não existe valor
}

Option<T> é um enum da biblioteca padrão — e é tão fundamental que suas variantes Some e None estão disponíveis sem qualificação:

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

fn main() {
    let resultado = dividir(10.0, 2.0);
    let invalido  = dividir(10.0, 0.0);

    match resultado {
        Some(v) => println!("Resultado: {v}"),
        None    => println!("Divisão inválida"),
    }

    match invalido {
        Some(v) => println!("Resultado: {v}"),
        None    => println!("Divisão inválida"),
    }
}

Saída:

Resultado: 5
Divisão inválida

A beleza está na obrigatoriedade: você não pode usar o valor dentro de um Option sem primeiro verificar se ele é Some ou None. O compilador não deixa. Isso elimina por completo os null pointer exceptions — em tempo de compilação.


match em profundidade

O match de Rust é muito mais poderoso que o switch de outras linguagens. Ele compara um valor contra padrões e executa o braço correspondente:

fn classificar_nota(nota: u32) -> &'static str {
    match nota {
        90..=100 => "Excelente",
        70..=89  => "Bom",
        50..=69  => "Regular",
        0..=49   => "Insuficiente",
        _        => "Nota inválida",
    }
}

fn main() {
    for nota in [100, 85, 60, 40, 150] {
        println!("{nota}: {}", classificar_nota(nota));
    }
}

Saída:

100: Excelente
85: Bom
60: Regular
40: Insuficiente
150: Nota inválida

O _ é o padrão curinga — captura qualquer valor não coberto pelos braços anteriores. Como o match é exaustivo, você precisa cobrir todos os casos possíveis ou usar _.


Guards em padrões

Você pode adicionar condições extras a um braço com if:

fn descrever_numero(n: i32) -> &'static str {
    match n {
        x if x < 0    => "negativo",
        0              => "zero",
        x if x % 2 == 0 => "positivo e par",
        _              => "positivo e ímpar",
    }
}

fn main() {
    for n in [-5, 0, 4, 7] {
        println!("{n}: {}", descrever_numero(n));
    }
}

Saída:

-5: negativo
0: zero
4: positivo e par
7: positivo e ímpar

Múltiplos padrões e desestruturação

O match suporta múltiplos padrões com |, e desestruturação de tuplas e structs:

fn main() {
    // Múltiplos padrões
    let x = 3;
    match x {
        1 | 2 => println!("um ou dois"),
        3 | 4 => println!("três ou quatro"),
        _     => println!("outro"),
    }

    // Desestruturação de tupla
    let ponto = (0, -2);
    match ponto {
        (0, 0) => println!("origem"),
        (x, 0) => println!("no eixo x: {x}"),
        (0, y) => println!("no eixo y: {y}"),
        (x, y) => println!("ponto ({x}, {y})"),
    }
}

Saída:

três ou quatro
no eixo y: -2

if let — match para um único caso

Quando você só se importa com uma variante e quer ignorar as demais, if let é mais conciso que match:

fn main() {
    let config: Option<&str> = Some("modo_escuro");

    // Com match — verboso para um único caso
    match config {
        Some(valor) => println!("Config: {valor}"),
        None => {}
    }

    // Com if let — mais limpo
    if let Some(valor) = config {
        println!("Config: {valor}");
    }

    // Com else opcional
    if let Some(valor) = config {
        println!("Usando: {valor}");
    } else {
        println!("Usando configuração padrão");
    }
}

Use if let quando tiver um único caso de interesse. Use match quando precisar tratar múltiplas variantes.


Métodos em enums

Assim como structs, enums podem ter métodos em blocos impl:

#[derive(Debug)]
enum Semaforo {
    Verde,
    Amarelo,
    Vermelho,
}

impl Semaforo {
    fn duracao_segundos(&self) -> u32 {
        match self {
            Semaforo::Verde    => 45,
            Semaforo::Amarelo  => 5,
            Semaforo::Vermelho => 40,
        }
    }

    fn pode_passar(&self) -> bool {
        matches!(self, Semaforo::Verde)
    }

    fn proximo(&self) -> Semaforo {
        match self {
            Semaforo::Verde    => Semaforo::Amarelo,
            Semaforo::Amarelo  => Semaforo::Vermelho,
            Semaforo::Vermelho => Semaforo::Verde,
        }
    }
}

fn main() {
    let mut estado = Semaforo::Verde;

    for _ in 0..6 {
        println!(
            "{:?} — {}s — {}",
            estado,
            estado.duracao_segundos(),
            if estado.pode_passar() { "SIGA" } else { "PARE" }
        );
        estado = estado.proximo();
    }
}

Saída:

Verde — 45s — SIGA
Amarelo — 5s — PARE
Vermelho — 40s — PARE
Verde — 45s — SIGA
Amarelo — 5s — PARE
Vermelho — 40s — PARE

Um programa completo: sistema de pagamentos

Vamos modelar um sistema simplificado de métodos de pagamento — um caso de uso real onde enums brilham:

#[derive(Debug)]
enum MetodoPagamento {
    CartaoCredito { numero: String, parcelas: u8 },
    Pix { chave: String },
    Boleto { vencimento_dias: u32 },
    Dinheiro,
}

impl MetodoPagamento {
    fn taxa(&self, valor: f64) -> f64 {
        match self {
            MetodoPagamento::CartaoCredito { parcelas, .. } => {
                if *parcelas > 1 {
                    valor * 0.03 * (*parcelas as f64)
                } else {
                    0.0
                }
            }
            MetodoPagamento::Pix { .. }     => 0.0,
            MetodoPagamento::Boleto { .. }  => 3.50,
            MetodoPagamento::Dinheiro       => 0.0,
        }
    }

    fn descricao(&self) -> String {
        match self {
            MetodoPagamento::CartaoCredito { numero, parcelas } => {
                let final_numero = &numero[numero.len()-4..];
                format!("Cartão ****{final_numero} em {parcelas}x")
            }
            MetodoPagamento::Pix { chave } => {
                format!("PIX para {chave}")
            }
            MetodoPagamento::Boleto { vencimento_dias } => {
                format!("Boleto — vence em {vencimento_dias} dias")
            }
            MetodoPagamento::Dinheiro => {
                String::from("Pagamento em dinheiro")
            }
        }
    }
}

fn processar_pagamento(valor: f64, metodo: &MetodoPagamento) {
    let taxa = metodo.taxa(valor);
    let total = valor + taxa;

    println!("── Pagamento ──────────────────");
    println!("Método : {}", metodo.descricao());
    println!("Valor  : R$ {valor:.2}");
    if taxa > 0.0 {
        println!("Taxa   : R$ {taxa:.2}");
    }
    println!("Total  : R$ {total:.2}");
    println!("────────────────────────────────\n");
}

fn main() {
    let pagamentos = vec![
        (150.0, MetodoPagamento::Pix {
            chave: String::from("ana@email.com")
        }),
        (500.0, MetodoPagamento::CartaoCredito {
            numero: String::from("1234567890121234"),
            parcelas: 3,
        }),
        (89.90, MetodoPagamento::Boleto {
            vencimento_dias: 5
        }),
        (30.0, MetodoPagamento::Dinheiro),
    ];

    for (valor, metodo) in &pagamentos {
        processar_pagamento(*valor, metodo);
    }
}

Saída:

── Pagamento ──────────────────
Método : PIX para ana@email.com
Valor  : R$ 150.00
Total  : R$ 150.00

── Pagamento ──────────────────
Método : Cartão ****1234 em 3x
Valor  : R$ 500.00
Taxa   : R$ 45.00
Total  : R$ 545.00

── Pagamento ──────────────────
Método : Boleto — vence em 5 dias
Valor  : R$ 89.90
Taxa   : R$ 3.50
Total  : R$ 93.40

── Pagamento ──────────────────
Método : Pagamento em dinheiro
Valor  : R$ 30.00
Total  : R$ 30.00

O que enums revelam sobre design

Modelar com enums força você a pensar em todos os estados possíveis do seu sistema antes de escrever a lógica. Essa é a essência do que a comunidade Rust chama de "make illegal states unrepresentable" — tornar estados inválidos impossíveis de existir no tipo.

Se um pagamento precisa obrigatoriamente ser PIX, cartão, boleto ou dinheiro, e nada mais, o enum garante isso. Não há como criar um MetodoPagamento com um quinto estado não previsto. O compilador é a documentação viva do seu domínio.


Fontes e leituras recomendadas

Comentários

Mais em Rust

Ownership — A Ideia que Muda Tudo
Ownership — A Ideia que Muda Tudo

Chegamos ao artigo mais importante da s&eacute;rie. Tudo que aprendemos at&ea...

Traits — Definindo Comportamento Compartilhado
Traits — Definindo Comportamento Compartilhado

&nbsp; Nos artigos anteriores criamos structs e enums para modelar dados. Ma...

Borrowing e Referências — Usando sem Possuir
Borrowing e Referências — Usando sem Possuir

&nbsp; No artigo anterior, aprendemos que ownership resolve o problema do ge...