- 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.