Modelando Dados com Structs e Enums
Duas perguntas impulsionam a maior parte da modelagem de dados: o que essa coisa tem, e o que essa coisa pode ser. Rust responde à primeira com structs e à segunda com enums, e a distinção entre elas não é estilística - reflete duas formas matemáticas diferentes que a maioria das outras linguagens mainstream confunde.
Uma struct agrupa campos que existem todos de uma vez: um User tem um id e um name e um email. Um enum descreve um valor que é exatamente uma de várias alternativas: uma Shape é um Circle ou um Square, nunca ambos, nunca nenhum dos dois. Manter essas duas formas distintas - e forçar todo uso de um enum através de matching exaustivo - é o que permite que Rust empurre uma categoria grande de bugs de "esqueci de lidar com um caso" do tempo de execução para o compilador.
Esta página é a âncora conceitual para a seção. Structs Básicas e Enums abordam a sintaxe na prática; esta página explica por que os dois tipos existem como um par e o que esse pareamento oferece.
Resumo
- Structs (tipos de produto) modelam "tem todos estes campos"; enums (tipos de soma) modelam "é exatamente uma destas variantes" - juntos, eles permitem que você modele dados de forma que apenas estados válidos possam ser construídos.
- Por que Importa: Muitos bugs de produção vêm de um valor estar em um estado que o código nunca esperou; tipos de soma mais matching exaustivo movem o ônus de notar "você perdeu um caso" dos testes e revisão de código para o compilador.
- Conceitos Chave: tipo de produto, tipo de soma, variante, matching exaustivo, tornar estados ilegais irrepresentáveis, pattern matching.
- Quando Usar Este Modelo: Ao projetar qualquer tipo que tenha um conjunto finito de modos distintos (estado de requisição, status de conexão, resultado de parse), e qualquer tipo onde uma combinação de campos seria de outra forma inválida em conjunto.
- Limitações / Trade-offs: Enums só podem conter formas de dados que você definiu antecipadamente, e adicionar uma variante é uma mudança que quebra compatibilidade para todo
matchexaustivo nesse tipo em toda a base de código - que é o mecanismo de segurança, mas significa que as mudanças se propagam para fora. - Tópicos Relacionados: tipos de dados algébricos,
Option/Resultcomo enums, pattern matching, máquinas de estado.
Fundamentos
Um tipo de produto é chamado assim porque o número de valores possíveis que ele pode conter é o produto dos valores possíveis de cada campo. Uma struct com um campo bool e um campo u8 pode representar 2 x 256 combinações distintas, e a struct do Rust é exatamente isso: cada campo está presente, o tempo todo, para cada instância.
struct User {
id: u64,
is_admin: bool,
}Um tipo de soma é chamado assim porque seus valores possíveis são a soma dos valores possíveis entre suas variantes, não o produto. O enum do Rust é um tipo de soma: um valor é uma variante, escolhida de uma lista fixa, e apenas os dados dessa variante existem para esse valor.
enum Connection {
Disconnected,
Connecting { attempt: u8 },
Connected { session_id: u64 },
}Um valor Connection nunca é Disconnected e Connected ao mesmo tempo, e nunca é nenhum dos dois. Compare isso com um padrão comum em linguagens sem tipos de soma: uma struct com um campo is_connected: bool e um campo separado e opcional session_id: Option<u64>, onde nada no tipo impede is_connected == false enquanto session_id ainda é Some(...). Essa combinação é sem sentido, mas o sistema de tipos não tem como dizer isso - é um bug esperando para que os dois campos errados discordem em tempo de execução.
Essa é a ideia central por trás de tornar estados ilegais irrepresentáveis: em vez de escrever código que verifica se uma combinação de campos faz sentido, você projeta o tipo de forma que a combinação sem sentido não possa ser construída em primeiro lugar.
Mecânicas e Interações
Enums ganham sua segurança através do matching exaustivo. Quando você usa match em um enum, o compilador exige que todas as variantes sejam tratadas, seja explicitamente ou através de um braço curinga _.
fn describe(c: &Connection) -> String {
match c {
Connection::Disconnected => "idle".into(),
Connection::Connecting { attempt } => format!("retry #{attempt}"),
Connection::Connected { session_id } => format!("session {session_id}"),
// adicione uma nova variante a Connection, e todo match como
// este em toda a base de código falhará na compilação até ser corrigido
}
}Este é o mecanismo, não apenas uma propriedade legal. Se Connection ganhar uma variante Reconnecting no próximo trimestre, todo match exaustivo em Connection em qualquer lugar da base de código se tornará um erro de compilação até que alguém decida o que esse novo estado significa em cada local de chamada. Em uma linguagem com um switch/default em tempo de execução ou uma cadeia if/else if sobre tags de string, a mudança equivalente compila bem e silenciosamente cai para o que quer que o caso default faça - o que é muito frequentemente o comportamento errado, descoberto mais tarde, em produção.
Structs e enums se combinam constantemente na prática. Variantes de enum podem conter dados em forma de struct (como Connecting { attempt: u8 } faz acima), e structs podem conter campos enum para representar uma peça de estado que varia. Option<T> e Result<T, E>, possivelmente os dois tipos mais usados em Rust, são ambos enums comuns construídos exatamente com esse vocabulário: Option<T> é Some(T) ou None, Result<T, E> é Ok(T) ou Err(E), e ambos são comparados exaustivamente como qualquer outro enum.
A desestruturação estende esse padrão para let bindings e parâmetros de função, não apenas para braços de match. let User { id, .. } = user; extrai um campo por padrão, e o mesmo raciocínio de exaustividade se aplica onde quer que um padrão seja necessário para contabilizar totalmente a forma de um valor.
Considerações Avançadas e Aplicações
Modelar um valor "um de vários estados" não é exclusivo de enums - outras abordagens existem, com trade-offs reais.
| Abordagem | Força | Fraqueza | Melhor Encaixe |
|---|---|---|---|
Enum + match exaustivo (Rust) | Completude imposta pelo compilador; combinações ilegais irrepresentáveis | Novas variantes são uma mudança que quebra compatibilidade em todos os lugares onde são comparadas | Qualquer conjunto fixo e fechado de estados mutuamente exclusivos |
| Struct com campos "tag" booleanos/de string | Familiar, flexível, fácil de adicionar campos incrementalmente | Nada impede combinações inválidas de campos; verificações são em tempo de execução e fáceis de pular | Prototipagem rápida onde os invariantes ainda não estão definidos |
| Hierarquia de classes / herança (OOP) | Extensível por terceiros sem modificar o tipo base | Aberto - o compilador não pode enumerar "todos os subtipos possíveis" para verificar exaustividade | Sistemas estilo plugin onde o conjunto de variantes é deliberadamente ilimitado |
Objetos de trait (dyn Trait) | Conjunto aberto de implementadores, polimorfismo em tempo de execução | Nenhuma verificação de exaustividade - por design, já que o conjunto é aberto | Extensibilidade definida por comportamento em vez de modelagem de estado fechado |
O custo de mudança que quebra compatibilidade ao adicionar uma variante de enum é um recurso deliberado, não um atrito para contornar. Significa que o compilador encontra todos os lugares onde seu código faz uma suposição sobre o conjunto fechado de possibilidades, no momento em que essa suposição se torna falsa, em vez de deixar para um futuro mantenedor descobrir lendo um relatório de bug. Para extensibilidade genuinamente aberta - plugins, comportamentos definidos pelo usuário - essa mesma rigidez é a ferramenta errada, que é por que Rust também oferece objetos de trait para casos onde o conjunto de "tipos de coisa" deve permanecer aberto (veja a seção de Traits & Generics).
Essa disciplina de modelagem escala para a arquitetura. Máquinas de estado mapeiam quase diretamente para enums, com cada estado como uma variante e cada transição como um método que consome uma variante e produz outra, de modo que uma transição inválida simplesmente não tem um caminho de código que a produza. A modelagem de domínio em sistemas maiores se baseia na mesma ideia: fazer o parse de entrada externa (JSON, uma submissão de formulário) em um enum o mais cedo possível converte "os dados são válidos" de uma preocupação contínua em tempo de execução para uma verificação de limite única, após a qual cada função downstream pode confiar na forma que recebeu.
Equívocos Comuns
- "Enums em Rust são como enums em C ou Java - um conjunto de constantes inteiras nomeadas." Variantes de enum Rust podem carregar dados arbitrários e de formas diferentes por variante (
Connecting { attempt: u8 }versusConnected { session_id: u64 }); eles são mais próximos de uniões marcadas do que dos enums inteiros simples de C. - "Uma struct com campos opcionais é basicamente a mesma coisa que um enum." Uma struct com vários campos
Option<T>ainda permite que todas as combinações deSome/Nonesejam construídas, incluindo as sem sentido; um enum restringe o valor a exatamente uma forma coerente por vez. - "Matching exaustivo é apenas digitação extra para o mesmo comportamento em tempo de execução." Ele muda quando um caso ausente é capturado - tempo de compilação em vez de tempo de execução - que é todo o benefício de segurança, não uma diferença cosmética.
- "Adicionar um braço curinga
_a um match é sempre bom." Um curinga absorve silenciosamente qualquer variante futura que você adicionar, o que derrota silenciosamente a verificação de exaustividade para aquele local de chamada - deve ser uma escolha deliberada, não um reflexo para fazer o compilador parar de reclamar. - "Você deve usar enums para tudo que tem 'algumas opções'." Se o conjunto de opções deve crescer indefinidamente a partir de fora do seu crate (plugins, comportamento definido pelo usuário), um trait é geralmente um encaixe melhor do que um enum, que assume um conjunto fechado e conhecido de variantes.
FAQs
Qual é a diferença entre um tipo de produto e um tipo de soma, em termos simples?
Um tipo de produto (struct) contém todos os seus campos de uma vez, então seus valores possíveis se multiplicam; um tipo de soma (enum) contém uma de suas variantes por vez, então seus valores possíveis se somam entre as variantes.
Por que variantes de enum podem conter dados diferentes umas das outras?
Porque cada variante representa um estado genuinamente diferente com informações potencialmente diferentes anexadas - um estado Connecting precisa de uma contagem de tentativas, um estado Connected precisa de um ID de sessão, e não há razão para forçar ambos os campos a existirem para cada estado.
Como o `match` exaustivo realmente previne bugs em tempo de compilação?
O compilador rastreia cada variante do tipo enum que está sendo comparado e exige um braço (ou um curinga) para cada uma. Se uma variante não for tratada, ou uma nova variante for adicionada posteriormente sem atualizar esse match, a compilação falha nesse local exato em vez de o programa rodar com um caso silenciosamente ignorado.
O `Option<T>` é realmente apenas um enum como qualquer outro?
Sim - Option<T> é definido como enum Option<T> { Some(T), None } na biblioteca padrão, sem magia especial de compilador além do que qualquer enum recebe. Compará-lo exaustivamente funciona exatamente como comparar um enum personalizado.
Quando devo usar um enum em vez de uma struct com um campo de status?
Quando o "status" implica que dados diferentes são relevantes ou válidos dependendo de qual status é. Se um status Connecting tem uma contagem de tentativas que não faz sentido para Disconnected, um variante enum por status impede que esse campo exista quando não deveria.
Adicionar uma nova variante de enum realmente quebra o código existente?
Sim, deliberadamente - qualquer match exaustivo nesse enum sem um braço curinga falha na compilação até que ele contabilize a nova variante. Este é o mecanismo que encontra todos os lugares onde seu código assumiu um conjunto fechado e menor de possibilidades.
O que significa "tornar estados ilegais irrepresentáveis" na prática?
Significa projetar o tipo de forma que um valor que não deveria existir - como "conectado mas sem ID de sessão", ou "desconectado mas ainda tentando novamente" - simplesmente não possa ser construído, em vez de depender de código em outro lugar para verificar e rejeitar essa combinação em tempo de execução.
Objetos de trait são um substituto para enums?
Não - eles resolvem um problema diferente. Enums modelam um conjunto fechado e conhecido de variantes com verificação de exaustividade; objetos de trait modelam um conjunto aberto de implementadores onde novas "espécies" podem ser adicionadas sem modificar o tipo original, ao custo de perder completamente a exaustividade.
Por que um braço curinga `_` em um match enfraquece essa segurança?
Um curinga corresponde a qualquer variante não explicitamente listada, incluindo aquelas adicionadas após o match ter sido escrito. Isso significa que uma adição futura de variante compila silenciosamente e cai no que quer que o curinga faça, o que derrota a garantia de "o compilador pega casos perdidos" para aquele local de chamada.
Como structs e enums se combinam em código real?
Constantemente - variantes de enum frequentemente carregam dados em forma de struct (campos nomeados por variante), e structs frequentemente têm campos tipados como um enum para representar uma peça de estado que varia. Result<T, E> dentro de um campo struct é um exemplo comum: a struct sempre existe, mas um de seus campos pode estar em um de vários estados.
Este estilo de modelagem é exclusivo do Rust?
Não - tipos de soma e exaustividade de pattern matching vêm da família de linguagens ML/Haskell e aparecem em Swift, Kotlin (sealed classes), TypeScript (discriminated unions), e outros. A versão do Rust é notável principalmente pela centralidade que tem no código idiomático, incluindo os próprios Option e Result da biblioteca padrão.
Qual é o custo de projetar tipos dessa forma?
Esforço de design inicial: você tem que realmente enumerar os estados válidos antes de escrever o código, em vez de adicionar campos e flags incrementalmente conforme as necessidades surgem. Esse custo tende a se pagar mais tarde, quando o conjunto fechado do enum captura um caso que você teria perdido manualmente.
Relacionados
- Structs Básicas - o guia prático de tipos de produto
- Enums - o guia prático de tipos de soma
- Pattern Matching com match - detalhes sobre mecânicas de matching exaustivo
- Option & Result como Enums - os próprios tipos de soma da biblioteca padrão
- Enums para Máquinas de Estado - aplicando este modelo a sistemas com estado
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.