32 points par GN⁺ 2025-12-07 | Aucun commentaire pour le moment. | Partager sur WhatsApp
  • Présentation d’habitudes de codage qui permettent de bloquer les bugs en amont en tirant pleinement parti du système de types et du compilateur de Rust
  • Présentation de cas de code smells fragiles tels que l’indexation de vecteurs, l’abus de Default, les match incomplets ou les paramètres booléens inutiles, avec leurs alternatives
  • Le principe central consiste à concevoir les structures de sorte que le compilateur impose les invariants, en s’appuyant sur le pattern matching, les champs privés ou l’attribut #[must_use]
  • Présentation concrète de techniques défensives au niveau du code réel : utilisation de TryFrom, déstructuration complète des structures, mutabilité temporaire, validation dans les constructeurs
  • Ces modèles sont essentiels pour garantir la stabilité lors du refactoring et améliorer la maintenabilité à long terme

Vue d’ensemble de la programmation défensive

  • Un emplacement accompagné du commentaire // this should never happen est un point où un invariant implicite est rompu
    • Dans la plupart des cas, le développeur n’a pas pris en compte toutes les conditions limites ni les futures modifications du code
  • Le compilateur Rust garantit la sûreté mémoire, mais des erreurs de logique métier restent possibles
  • De petits idiomes acquis avec des années d’expérience pratique améliorent fortement la qualité du code

Code smell : indexation de vecteurs

  • Une forme comme if !vec.is_empty() { let x = &vec[0]; } présente un risque de panic à l’exécution, car la vérification de longueur et l’indexation sont séparées
  • En utilisant le pattern matching sur les slices (match vec.as_slice()), le compilateur force la vérification de tous les états
    • Il devient possible de traiter explicitement tous les cas : vecteur vide, élément unique, doublons, etc.
  • C’est un exemple représentatif d’une conception où le compilateur garantit les invariants

Code smell : usage inconsidéré de Default

  • ..Default::default() entraîne un risque d’omission lors de l’ajout de nouveaux champs ainsi qu’un problème de valeurs implicites
  • Si tous les champs sont initialisés explicitement, le compilateur impose aussi l’initialisation des nouveaux champs
  • Avec une forme comme let Foo { field1, field2, .. } = Foo::default();, on peut déstructurer une structure par défaut puis redéfinir sélectivement certains champs
    • Cela permet d’équilibrer conservation des valeurs par défaut et surcharge explicite

Code smell : implémentations de traits fragiles

  • En déstructurant complètement les champs d’une structure lors d’une comparaison, l’ajout d’un nouveau champ provoque une erreur de compilation servant d’alerte
    • Exemple : dans une implémentation de PartialEq, let Self { size, toppings, .. } = self;
  • Si un nouveau champ comme extra_cheese est ajouté, la logique de comparaison doit être réexaminée
  • Le même principe s’applique aussi à d’autres traits comme Hash, Debug ou Clone

Code smell : TryFrom requis à la place de From

  • Quand une conversion ne peut pas toujours réussir, il faut indiquer explicitement la possibilité d’échec avec TryFrom plutôt qu’utiliser From
  • L’usage de unwrap_or_else est un signal qui masque un échec potentiel ; une stratégie de fail fast est plus sûre

Code smell : match incomplet

  • Un pattern attrape-tout comme _ => {} présente un risque d’omission lors de l’ajout d’un nouveau variant
  • Si tous les variants sont listés explicitement, le compilateur avertit lorsqu’un nouveau cas n’est pas pris en charge
  • La même logique peut être regroupée avec une forme comme Variant3 | Variant4

Code smell : abus du placeholder _

  • N’utiliser que _ rend peu clair quels champs ou variables ont été omis
  • Des noms explicites comme has_fuel: _, has_crew: _ améliorent la lisibilité

Modèle : mutabilité temporaire

  • Quand des données ne doivent être mutables que pendant l’initialisation, on peut utiliser une forme comme let mut data = ...; data.sort(); let data = data;
  • En exploitant la portée des blocs, on peut éviter d’exposer la variable temporaire à l’extérieur
    • Exemple : let data = { let mut d = get_vec(); d.sort(); d };
  • Cela permet aussi de délimiter clairement les portées lors d’une initialisation impliquant plusieurs variables temporaires

Modèle : forcer la validation dans les constructeurs

  • Lors de la création d’une structure, il faut imposer le passage par une logique de validation
    • En ajoutant un champ _private: (), la création directe depuis l’extérieur devient impossible
    • L’attribut #[non_exhaustive] empêche la création hors de la crate et signale une future extensibilité
  • Pour imposer cela aussi dans les modules internes, on peut utiliser une structure de modules imbriqués avec un type privé (Seal)
    • Comme Seal n’existe qu’en interne, la création directe en dehors de new() devient impossible
  • En rendant les champs privés et en fournissant des getters, on préserve l’état invariant
  • Critères d’application
    • Bloquer le code externe : _private ou #[non_exhaustive]
    • Bloquer le code interne : module privé + Seal
    • Transformer la logique de validation en garantie assurée par le compilateur

Modèle : utilisation de l’attribut #[must_use]

  • #[must_use] permet d’éviter qu’une valeur de retour importante soit ignorée
    • Exemple : #[must_use = "Configuration must be applied to take effect"]
  • Si l’utilisateur ignore la valeur de retour, le compilateur émet un avertissement
  • C’est un mécanisme défensif simple mais puissant, largement utilisé aussi dans la bibliothèque standard, notamment pour Result

Code smell : paramètres booléens

  • Une forme comme fn process_data(..., compress: bool, encrypt: bool, validate: bool) présente un sens peu clair et un risque d’erreur d’ordre
  • Des types comme enum Compression ou enum Encryption permettent d’exprimer explicitement l’intention
  • S’il existe plusieurs options, on peut utiliser une structure de paramètres (Params struct)
    • Des méthodes prédéfinies comme ProcessDataParams::production() améliorent la réutilisabilité
  • Lors de l’ajout de nouvelles options, l’impact sur les appels existants est minimisé

Automatisation avec les lints Clippy

  • Les principaux modèles défensifs peuvent être vérifiés automatiquement avec des lints Clippy
    • indexing_slicing : interdit l’indexation directe
    • fallible_impl_from : recommande TryFrom au lieu de From
    • wildcard_enum_match_arm : interdit le pattern _
    • fn_params_excessive_bools : avertit en cas d’excès de paramètres booléens
    • must_use_candidate : propose des candidats pour #[must_use]
  • On peut les appliquer à tout le projet via #![deny(clippy::...)] ou une configuration dans Cargo.toml

Conclusion

  • Le cœur de la programmation défensive en Rust consiste à tirer activement parti du système de types et du compilateur pour rendre les invariants explicites et vérifiables
  • Ces modèles contribuent à sécuriser les refactorings, minimiser les risques de bugs et renforcer la maintenabilité à long terme
  • C’est une approche qui met en pratique le principe suivant : « le meilleur bug est celui qui ne compile pas »

Aucun commentaire pour le moment.

Aucun commentaire pour le moment.