ProgrammationDéveloppeur Rust Backend

Comment fonctionne le système de constructeurs et de méthodes de fabrication en Rust ? Quels patterns de création d'objets sont appliqués, comment l'invariant est-il assuré et comment les structures sont-elles initialisées ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

En Rust, il n'existe pas de constructeurs traditionnels comme en C++ ou Java, mais pour créer des objets de types, on utilise généralement des fonctions associées (souvent nommées new) et ce que l'on appelle des méthodes de fabrication. Cela est lié à l'histoire du langage, qui accorde une attention particulière à la sécurité et à l'initialisation explicite : seule une fonction écrite et appelée explicitement est responsable de l'initialisation correcte de chaque champ de la structure.

Historique de la question

À l'origine, en Rust, l'initialisation des structures permettait l'affectation directe de tous les champs (système de "struct literal"). Cependant, pour assurer l'invariant, masquer les détails et mettre en œuvre des vérifications supplémentaires, on pratique l'utilisation de méthodes de fabrication (impl SomeStruct { fn new(...) -> Self { ... } }) ou même la généralisation par des templates (patron de construction).

Problème

Les principaux enjeux sont d'éviter les objets partiellement initialisés et de rendre impossible l'utilisation de structures dans un état non valide. Cela est particulièrement critique pour les structures complexes (par exemple, celles liées à des ressources — fichiers, sockets, etc.), où l'initialisation manuelle de tous les champs est source d'erreurs.

Solution

Il est recommandé en Rust de créer des méthodes de fabrication qui retournent un objet complètement initialisé, effectuant au besoin une validation et masquant les détails de l'instanciation.

Exemple de code :

struct User { username: String, age: u8, } impl User { pub fn new(username: String, age: u8) -> Option<Self> { if age >= 18 { Some(Self { username, age }) } else { None } } } fn main() { let user = User::new("Alice".to_string(), 20); // user: Option<User>, traiter l'erreur en toute sécurité }

Caractéristiques clés :

  • Pas de constructeurs automatiques comme dans d'autres langages, mais il y a une implémentation via des fonctions associées (fn new).
  • Les méthodes de fabrication permettent d'effectuer des vérifications d'intégrité et de cacher les détails de la mise en œuvre interne.
  • Le patron de construction est efficacement soutenu lorsqu'il est nécessaire d'avoir de nombreux paramètres optionnels et une initialisation progressive.

Questions pièges.

Peut-on avoir des champs privés dans une structure, de sorte qu'il soit impossible de créer des instances directement en dehors du module ?

Oui, si tous les champs de la structure sont privés et que l'on propose uniquement des méthodes de fabrication publiques, la structure ne pourra pas être initialisée directement en dehors de son module.

Le méthode de fabrication doit-elle toujours s'appeler new ?

Non, c'est une convention, mais pas une obligation. Pour différentes stratégies d'initialisation, on peut utiliser des noms tels que "with_capacity", "from_config", "from_env", etc.

Les constructeurs peuvent-ils être privés ?

Oui, si la fonction associée est déclarée comme fn new(...) -> Self sans le modificateur pub, elle ne pourra pas être appelée en dehors de ce module. Cela permet, par exemple, d'implémenter un singleton, une factory stricte ou une initialisation masquée.

Erreurs typiques et anti-patterns

  • Création d'une structure avec des champs publics, permettant de contourner les invariants ou d'obtenir un objet dans un état non valide.
  • Ne pas utiliser de méthodes de fabrication pour des structures complexes avec des ressources externes.
  • Confusion entre un constructeur et une méthode de validation : par exemple, retourner Result/Option, bien que l'échec d'initialisation implique une logique à un autre niveau.

Exemple de la vie réelle

Cas négatif

Utilisation directe d'une structure avec des champs ouverts, sans méthode de fabrication :

struct Connection { fd: i32, timeout: u64, } let c = Connection { fd: -1, timeout: 10 }; // fd: -1 n'est pas valide pour un descripteur !

Avantages :

  • Rapidité de prototypage.
  • Moins de code.

Inconvénients :

  • Pas de garantie que l'objet ne sera pas dans un état non valide.
  • Les erreurs n'apparaissent qu'à l'exécution.

Cas positif

Utilisation de champs privés et d'une méthode de fabrication :

pub struct Connection { fd: i32, timeout: u64, } impl Connection { pub fn new(fd: i32, timeout: u64) -> Option<Self> { if fd >= 0 { Some(Self { fd, timeout }) } else { None } } }

Avantages :

  • Le compilateur empêchera la création d'un objet non valide.
  • Gestion explicite des vérifications.

Inconvénients :

  • Volume de code légèrement augmenté.
  • Ce patron n'est pas nécessaire pour toutes les structures simples.