- Apresenta hábitos de codificação para prevenir bugs antecipadamente, aproveitando ativamente o sistema de tipos e o compilador do Rust
- Mostra exemplos de code smells frágeis, como indexação de vetores, abuso de
Default, match incompleto e parâmetros booleanos desnecessários, além de explicar alternativas
- O princípio central é projetar a estrutura para que o compilador imponha invariantes, usando pattern matching, campos privados e o atributo
#[must_use]
- Apresenta de forma concreta técnicas defensivas em nível de código real, como uso de
TryFrom, desmontagem completa de structs, mutabilidade temporária e validação em construtores
- Esses padrões são essenciais para garantir estabilidade em refatorações e melhorar a manutenção no longo prazo
Visão geral da programação defensiva
- Pontos com comentários como
// this should never happen indicam locais onde invariantes implícitos estão sendo quebrados
- Na maioria dos casos, o desenvolvedor não considerou todas as condições de contorno ou futuras mudanças no código
- O compilador do Rust garante segurança de memória, mas erros de lógica de negócio ainda podem acontecer
- Pequenos padrões habituais (idioms) adquiridos ao longo de anos de prática melhoram bastante a qualidade do código
Code Smell: indexação de vetores
- A forma
if !vec.is_empty() { let x = &vec[0]; } apresenta risco de pânico em runtime, pois a checagem de tamanho e a indexação estão separadas
- Ao usar pattern matching em slices (
match vec.as_slice()), o compilador força a verificação de todos os estados
- É possível tratar explicitamente todos os casos, como vetor vazio, elemento único e elementos duplicados
- Este é um exemplo representativo de projetar o código para que o compilador garanta invariantes
Code Smell: uso indiscriminado de Default
..Default::default() pode causar risco de omissão ao adicionar novos campos e problemas de definição implícita de valores
- Inicializar todos os campos explicitamente faz com que o compilador obrigue a configurar campos novos
- Com a forma
let Foo { field1, field2, .. } = Foo::default();, é possível desmontar a estrutura padrão e depois sobrescrever seletivamente
- Isso equilibra a preservação de valores padrão com overrides explícitos
Code Smell: implementação frágil de Trait
- Ao desmontar completamente os campos de uma struct durante a comparação, a adição de novos campos gera erro de compilação como alerta
- Ex.: ao implementar
PartialEq, usar let Self { size, toppings, .. } = self;
- Se um novo campo como
extra_cheese for adicionado, isso força a revisão da lógica de comparação
- O mesmo princípio pode ser aplicado a outros traits como
Hash, Debug e Clone
Code Smell: necessidade de TryFrom em vez de From
- Quando uma conversão nem sempre tem sucesso, deve-se usar
TryFrom em vez de From para explicitar a possibilidade de falha
- Usar
unwrap_or_else é um sinal de que uma falha potencial está sendo escondida, e a abordagem de falhar cedo (fail fast) é mais segura
Code Smell: match incompleto
- Padrões catch-all como
_ => {} trazem risco de omissão quando um novo variant é adicionado
- Ao listar explicitamente todos os variants, o compilador alerta sobre casos novos não tratados
- A mesma lógica pode ser agrupada com formas como
Variant3 | Variant4
Code Smell: abuso do placeholder _
- Usar apenas
_ torna pouco claro quais variáveis foram omitidas
- Formas como
has_fuel: _, has_crew: _ melhoram a legibilidade com nomes explícitos
Padrão: mutabilidade temporária (Temporary Mutability)
- Quando os dados devem ser mutáveis apenas durante a inicialização, use a forma
let mut data = ...; data.sort(); let data = data;
- Com escopo de bloco, é possível evitar expor a variável temporária para fora
- Ex.:
let data = { let mut d = get_vec(); d.sort(); d };
- Isso também permite separar claramente o escopo em processos de inicialização com várias variáveis temporárias
Padrão: forçar validação no construtor
- Ao criar uma struct, é possível forçar a passagem pela lógica de validação
- Ao adicionar o campo
_private: (), a criação direta por código externo se torna impossível
- O atributo
#[non_exhaustive] bloqueia a criação fora do crate e sinaliza expansão futura
- Para forçar isso também dentro de módulos internos, usa-se uma estrutura de módulos aninhados com tipo privado (
Seal)
- Como
Seal existe apenas internamente, a criação direta fora de new() se torna impossível
- Ao manter os campos privados e fornecer getters, o estado imutável é preservado
- Critérios de aplicação
- Bloquear código externo:
_private ou #[non_exhaustive]
- Bloquear código interno: módulo privado +
Seal
- Transformar a lógica de validação em uma garantia no nível do compilador
Padrão: uso do atributo #[must_use]
#[must_use] evita que valores de retorno importantes sejam ignorados
- Ex.:
#[must_use = "Configuration must be applied to take effect"]
- Se o usuário ignorar o valor retornado, o compilador emite um aviso
- É uma medida defensiva simples, mas poderosa, amplamente usada também na biblioteca padrão, como em
Result
Code Smell: parâmetros booleanos
- A forma
fn process_data(..., compress: bool, encrypt: bool, validate: bool) traz significado ambíguo e risco de erro na ordem dos argumentos
- Com
enum Compression, enum Encryption etc., é possível expressar a intenção de forma explícita
- Quando há várias opções, use uma struct de parâmetros (
Params struct)
- Métodos de predefinição como
ProcessDataParams::production() melhoram a reutilização
- Ao adicionar novas opções, o impacto nas chamadas existentes é minimizado
Automação com Clippy Lints
- Os principais padrões defensivos podem ser verificados automaticamente com lints do Clippy
indexing_slicing: proíbe indexação direta
fallible_impl_from: recomenda TryFrom em vez de From
wildcard_enum_match_arm: proíbe padrão _
fn_params_excessive_bools: alerta sobre excesso de parâmetros booleanos
must_use_candidate: sugere candidatos para #[must_use]
- É possível aplicar isso ao projeto inteiro com
#![deny(clippy::...)] ou configurações no Cargo.toml
Conclusão
- O núcleo da programação defensiva em Rust é usar ativamente o sistema de tipos e o compilador para tornar invariantes explícitos e verificáveis
- Esses padrões ajudam a garantir estabilidade em refatorações, minimizar a chance de bugs e reforçar a manutenção no longo prazo
- É uma abordagem que coloca em prática o princípio de que “o melhor bug é o bug que não compila”
1 comentários
Comentário no Hacker News
Gostei do texto. Mas o exemplo de PizzaOrder parece concentrar preocupações demais em uma única struct
Se a intenção é excluir
ordered_atda comparação, acho melhor separar em duas structs,PizzaDetailsePizzaOrderAssim, ao implementar
PartialEq, fica claro que apenasdetailsdeve ser comparadoSe a hora do pedido for diferente, então não é o mesmo pedido, então definir isso como igual no nível do tipo é arriscado
Não há problema em colocar
PartialEqemPizzaDetails, mas a lógica de comparação de pedidos deveria ficar em uma função de negócio separadaPizzaDetails, essa mudança pode afetar a lógica de deduplicação de pizzasO ideal é usar struct apenas para agrupar dados
Para evitar que mudanças afetem outras partes, também daria para considerar um tipo separado como
PizzaComparatorouPizzaFlavorSeria bom se, como no Protobuf, fosse possível ter anotações de campo como
{important_to_flavour=true}Por exemplo, se quiser comparar strings sem diferenciar maiúsculas de minúsculas, como faria essa separação?
Uma das coisas realmente incríveis no Rust é que muitas vezes programação defensiva nem é necessária
Graças às regras de posse e de referências, é possível garantir que o acesso a certo objeto seja único em todo o programa
Referências não podem ser null, e smart pointers também não podem ser null
O sistema de tipos também garante que, ao transferir a posse de
self, chamadas de método posteriores ficam impossíveisCom isso, segurança entre threads, tempos de vida e possibilidade de clonagem são verificados globalmente em tempo de compilação
Em outras linguagens, os benefícios que você obtém mantendo imutabilidade num estilo funcional, o Rust impõe pelo sistema de tipos
O tema do artigo eram bugs lógicos que nem o borrow checker consegue capturar
Acho sensato evitar indexação direta em arrays ou vetores
No dia do incidente do unwrap da Cloudflare, eu também encontrei um bug em que uma slice passava do fim de um vetor
Depois disso, mudei para uma abordagem baseada em iteradores e passei a me sentir muito mais seguro
O
unwrapdo Rust é igual aoassertdo C. Se falhar, só está cumprindo o papel de informar o problemaAinda é perfeitamente possível escrever bugs em Rust
Um dos hábitos contra os quais desenvolvedores Rust precisam se defender é o de adicionar dependências de crate desnecessárias
Rust tende a incentivar esse hábito. Por exemplo, o fato de o Rust Book usar a crate
randcomo exemplo padrão também cria esse climaClaro, isso foi uma escolha estratégica para permitir trocar facilmente pacotes relacionados a criptografia, mas ainda assim é um problema isso virar hábito
Mas depois entendi a intenção e mudei de opinião
A implementação de igualdade parcial foi interessante
Outra coisa que me intriga é o uso de enum para evitar parâmetros booleanos
Eu uso uma struct que encapsula bool, mas acho ruim não poder tratá-la como um bool comum
Fico pensando se existe alguma forma de usar enum como se fosse bool
Costumo lidar com isso agrupando a lógica necessária em uma Trait, ou adicionando métodos comuns em um bloco
impl <Enum>Assim fica legível e o comportamento de cada membro é definido com clareza
impl Deref, mas não sei se seria uma boa ideiaA expressão
matchdo primeiro exemplo parece exagerada demaisVec.first()ouVec.iter().nth(0)são mais claros e mais adequados à intençãomatchacaba virando uma solução mais complexa que o problemaSe dá para eliminar o
if, então também dá para eliminar omatch, então não há diferença do ponto de vista de segurançafirst()é muito mais conciso e claromatchtem valor por induzir você a também tratar o caso de “há um ou mais elementos”Ou seja, ele revela o princípio de evitar separar a verificação do código que depende dela
Sempre que leio textos assim, fico me perguntando por que não existe uma equipe dedicada a monitorar padrões de código
Seria bom ter uma equipe que observasse os padrões da base de código no longo prazo, como um SOC ou QA
Ferramentas automatizadas de detecção de code smell têm limitações
Ela cuida de regras de lint, documentação, treinamento de desenvolvedores e manutenção de bibliotecas comuns
Quando vários times repetem o mesmo problema, ela projeta uma API central para unificar isso
Mas, na prática, quando o código chega a milhões de linhas, é muito difícil de administrar
Tenho pensado em como incentivar esses bons padrões de código dentro do time
Durante code reviews, isso muitas vezes vira uma “discussão de estilo” e acaba sendo improdutivo
Mas, curiosamente, quando o linter mostra um aviso, esse tipo de discussão quase desaparece
Foi realmente muito útil quando a Trait
TryFromfoi adicionada na versão 1.34É bem possível que o código que usava
unwrap_or_else()seja um resquício de antes dissoA documentação da Trait
Fromagora explica com muita clareza quando ela deve ser implementadaunwrap_or_else()soa como se fosse “dar uma ordem ameaçadora ao computador”Acho que esses padrões de programação defensiva também podem ajudar a melhorar a qualidade da geração de código por IA em larga escala
O feedback concreto fornecido pelo Clippy e pelo compilador Rust pode ter um papel importante para ajudar agentes de IA a cometer menos erros e seguir na direção certa