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
- The Rust Programming Language, Cap. 6 — Enums and Pattern Matching — https://doc.rust-lang.org/book/ch06-00-enums.html
- Rust by Example — Enums — https://doc.rust-lang.org/rust-by-example/custom_types/enum.html
- The Rust Reference — Match expressions — https://doc.rust-lang.org/reference/expressions/match-expr.html
- "Making Illegal States Unrepresentable" — artigo seminal de Scott Wlaschin adaptado para Rust — https://geeklaunch.io/blog/make-invalid-states-unrepresentable/
- Tony Hoare — "Null References: The Billion Dollar Mistake" — apresentação original — https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/
- Rustlings, seção
enums— https://github.com/rust-lang/rustlings