32 pontos por GN⁺ 2025-12-07 | 1 comentários | Compartilhar no WhatsApp
  • 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

 
GN⁺ 2025-12-07
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_at da comparação, acho melhor separar em duas structs, PizzaDetails e PizzaOrder
    Assim, ao implementar PartialEq, fica claro que apenas details deve ser comparado

    • Boa observação. Mas ainda assim acho que, em termos lógicos, isso é uma modelagem incorreta
      Se 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 PartialEq em PizzaDetails, mas a lógica de comparação de pedidos deveria ficar em uma função de negócio separada
    • A abordagem de separar a estrutura é boa, mas o problema é que, ao modificar PizzaDetails, essa mudança pode afetar a lógica de deduplicação de pizzas
      O ideal é usar struct apenas para agrupar dados
      Para evitar que mudanças afetem outras partes, também daria para considerar um tipo separado como PizzaComparator ou PizzaFlavor
      Seria bom se, como no Protobuf, fosse possível ter anotações de campo como {important_to_flavour=true}
    • Separar a estrutura apenas por causa de uma forma diferente de comparação não é algo generalizável
      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íveis
    Com isso, segurança entre threads, tempos de vida e possibilidade de clonagem são verificados globalmente em tempo de compilação

    • Também acho que a verdadeira vantagem do Rust está nas coisas com as quais você “não precisa se preocupar”
      Em outras linguagens, os benefícios que você obtém mantendo imutabilidade num estilo funcional, o Rust impõe pelo sistema de tipos
    • Mas este comentário parece não ter relação com o artigo original
      O tema do artigo eram bugs lógicos que nem o borrow checker consegue capturar
    • O conteúdo do artigo focava principalmente em padrões de código para evitar erros lógicos ao melhorar o programa de forma iterativa
  • 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

    • Não acho que o incidente do unwrap precise ser tratado como um “incidente”
      O unwrap do Rust é igual ao assert do C. Se falhar, só está cumprindo o papel de informar o problema
      Ainda é perfeitamente possível escrever bugs em Rust
    • No fim, é o mesmo problema. O pessoal do Rust fala em abandonar C, mas em C também é comum usar handles em vez de índices
  • 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 rand como exemplo padrão também cria esse clima
    Claro, isso foi uma escolha estratégica para permitir trocar facilmente pacotes relacionados a criptografia, mas ainda assim é um problema isso virar hábito

    • Também tive rejeição ao Rust no começo por causa desse exemplo
      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

    • Eu também quase sempre prefiro enum + match!
      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
    • Talvez desse para usar algo como impl Deref, mas não sei se seria uma boa ideia
  • A expressão match do primeiro exemplo parece exagerada demais
    Vec.first() ou Vec.iter().nth(0) são mais claros e mais adequados à intenção

    • Também concordo. Usar match acaba virando uma solução mais complexa que o problema
      Se dá para eliminar o if, então também dá para eliminar o match, então não há diferença do ponto de vista de segurança
      first() é muito mais conciso e claro
    • Para expressar o mesmo comportamento de forma mais simples, também dá para usar exactly_one do itertools
    • Ainda assim, match tem 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

    • Na nossa empresa (cerca de 300 pessoas), existe uma equipe dedicada a dívida técnica com esse papel
      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
    • A maioria das grandes empresas de tecnologia tem times assim
      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 TryFrom foi adicionada na versão 1.34
    É bem possível que o código que usava unwrap_or_else() seja um resquício de antes disso
    A documentação da Trait From agora explica com muita clareza quando ela deve ser implementada

    • Ainda estou aprendendo Rust, mas acho engraçado como o nome unwrap_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