Enums: tipos soma em Rust

1. Introdução aos Enums em Rust

Enums (enumerações) em Rust são muito mais poderosos do que em linguagens tradicionais como C ou Java. Enquanto em C um enum é meramente um conjunto de constantes inteiras nomeadas, em Rust os enums são tipos soma — um conceito da teoria dos tipos que significa que um valor pode ser exatamente uma de várias possibilidades diferentes.

A diferença fundamental é que cada variante de um enum em Rust pode carregar seus próprios dados, de tipos potencialmente distintos. Isso permite modelar domínios complexos de forma segura e expressiva.

// Exemplo básico de enum
enum DiaDaSemana {
    Segunda,
    Terca,
    Quarta,
    Quinta,
    Sexta,
    Sabado,
    Domingo,
}

2. Definindo e instanciando Enums

A sintaxe de definição é direta: usamos a palavra-chave enum, seguida pelo nome e pelas variantes dentro de chaves. Cada variante pode ser simples ou conter dados associados.

// Variantes simples
enum Cor {
    Vermelho,
    Verde,
    Azul,
}

// Variantes com dados associados
enum EnderecoIP {
    V4(u8, u8, u8, u8),  // quatro octetos
    V6(String),           // endereço como string
}

// Criando valores
let cor_favorita = Cor::Azul;
let localhost = EnderecoIP::V4(127, 0, 0, 1);
let ipv6_teste = EnderecoIP::V6(String::from("::1"));

Enums também podem ter discriminantes inteiros explícitos, úteis para interoperabilidade:

enum StatusHttp {
    Ok = 200,
    NotFound = 404,
    InternalServerError = 500,
}

let codigo = StatusHttp::NotFound as u32; // converte para 404

3. Variantes com Dados: Tipos Produto dentro de Tipos Soma

O poder real dos enums em Rust vem da capacidade de associar dados diferentes a cada variante. Isso combina tipos produto (structs, tuplas) dentro de um tipo soma.

enum Mensagem {
    Sair,
    Mover(i32, i32),           // tupla com dados
    Escrever(String),          // dado único
    MudarCor { r: u8, g: u8, b: u8 },  // struct anônima
}

// Aninhamento com structs nomeadas
struct Ponto {
    x: f64,
    y: f64,
}

enum Forma {
    Circulo { raio: f64 },
    Retangulo { largura: f64, altura: f64 },
    Ponto(Ponto),  // struct como dado associado
}

4. Pattern Matching com match

O match é a ferramenta principal para trabalhar com enums. Ele permite desestruturar cada variante e extrair seus dados de forma segura e exaustiva.

fn processar_mensagem(msg: Mensagem) {
    match msg {
        Mensagem::Sair => {
            println!("Encerrando...");
        }
        Mensagem::Mover(x, y) => {
            println!("Movendo para ({}, {})", x, y);
        }
        Mensagem::Escrever(texto) => {
            println!("Texto: {}", texto);
        }
        Mensagem::MudarCor { r, g, b } => {
            println!("Nova cor: RGB({}, {}, {})", r, g, b);
        }
    }
}

// O compilador garante exaustividade!
// O pattern _ (coringa) captura qualquer variante não listada
fn dia_eh_util(dia: DiaDaSemana) -> bool {
    match dia {
        DiaDaSemana::Sabado | DiaDaSemana::Domingo => false,
        _ => true,  // todas as outras variantes
    }
}

5. O Enum Option<T>: Eliminando Null

Rust não tem null. Em vez disso, usa o enum Option<T> para representar a presença ou ausência de um valor:

// Definição padrão (já na biblioteca padrão)
// enum Option<T> {
//     None,
//     Some(T),
// }

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

let resultado = dividir(10.0, 2.0);

// Formas seguras de extrair o valor
if let Some(valor) = resultado {
    println!("Resultado: {}", valor);
}

// Ou usando métodos
let valor = resultado.unwrap_or(0.0);  // valor padrão se None
let valor = resultado.expect("Divisão falhou!");  // mensagem de erro se None

// Verificações booleanas
if resultado.is_some() {
    println!("Temos um resultado!");
}

6. O Enum Result<T, E>: Tratamento de Erros

Enquanto Option lida com ausência, Result lida com falhas que carregam informações sobre o erro:

// Definição padrão
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }

use std::fs::File;
use std::io::{self, Read};

fn ler_arquivo(caminho: &str) -> Result<String, io::Error> {
    let mut arquivo = File::open(caminho)?;  // operador ? propaga o erro
    let mut conteudo = String::new();
    arquivo.read_to_string(&mut conteudo)?;
    Ok(conteudo)
}

// Uso com match
match ler_arquivo("dados.txt") {
    Ok(conteudo) => println!("Conteúdo: {}", conteudo),
    Err(erro) => eprintln!("Erro ao ler arquivo: {}", erro),
}

// O operador ? simplifica a propagação em funções que retornam Result
fn processar() -> Result<(), io::Error> {
    let dados = ler_arquivo("config.txt")?;  // propaga erro automaticamente
    println!("Configuração: {}", dados);
    Ok(())
}

7. Métodos e Blocos impl para Enums

Enums podem ter métodos implementados, assim como structs:

enum Mensagem {
    Sair,
    Mover(i32, i32),
    Escrever(String),
}

impl Mensagem {
    fn executar(&self) {
        match self {
            Mensagem::Sair => println!("Tchau!"),
            Mensagem::Mover(x, y) => println!("Movendo para ({}, {})", x, y),
            Mensagem::Escrever(texto) => println!("{}", texto),
        }
    }

    fn descricao(&self) -> String {
        match self {
            Mensagem::Sair => String::from("Comando de saída"),
            Mensagem::Mover(_, _) => String::from("Comando de movimento"),
            Mensagem::Escrever(_) => String::from("Comando de escrita"),
        }
    }
}

let msg = Mensagem::Mover(10, 20);
msg.executar();
println!("Tipo: {}", msg.descricao());

8. Enums Recursivos e o Uso de Box

Enums recursivos (que contêm a si mesmos) precisam de alocação no heap porque o tamanho do enum seria infinito em tempo de compilação. Usamos Box para isso:

// Lista encadeada usando enum recursivo
enum Lista {
    Cons(i32, Box<Lista>),  // Box aloca no heap
    Nil,                     // fim da lista
}

use Lista::{Cons, Nil};

fn main() {
    let lista = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

    // Percorrendo a lista
    let mut atual = &lista;
    loop {
        match atual {
            Cons(valor, proximo) => {
                print!("{} ", valor);
                atual = proximo;
            }
            Nil => {
                println!();
                break;
            }
        }
    }
}

O Box permite que o compilador saiba o tamanho do enum (sempre o tamanho de um ponteiro para o heap), resolvendo o problema de tamanho indeterminado.


Referências