Enums
Esta aula introduz enums em Rust, mostrando como definir variantes simples e com dados, explorando o enum Option e discutindo casos de uso comuns como representação de estados e tratamento de erros.
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
- The Rust Programming Language - Defining an Enum
- The Rust Programming Language - The match Control Flow Operator
- The Rust Programming Language - Concise Control Flow with if let
- Rust Standard Library - Option
- Rust by Example - Enums
Exercícios
Crie um enum chamado
Formacom variantesCirculo(f64)(raio),Retangulo { largura: f64, altura: f64 }eTriangulo { base: f64, altura: f64 }. Escreva uma função que recebe umaFormae 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)); }Implemente uma função que recebe um
Option<i32>e retorna o dobro do valor se forSome, ou 0 se forNone. Usematch.✓ 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); }Crie um enum
StatusPedidocom variantesPendente,Processando,EnviadoeEntregue. 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); }Escreva uma função que recebe uma lista de
Option<i32>e retorna a soma de todos os valoresSome. Ignore osNone.✓ 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); }Defina um enum
Resultado<T, E>com variantesSucesso(T)eErro(E). Implemente uma função que simula uma operação de divisão que retornaResultado<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), } }