Le pattern Newtype
Fait partie de la section Object Oriented Programming du Journey Rust de Coddy. Leçon 54 sur 61.
Parfois, vous avez deux valeurs du même type qui représentent des choses complètement différentes. L'adresse email d'un utilisateur et son mot de passe peuvent tous deux être des valeurs String, mais les intervertir accidentellement dans un appel de fonction serait un bug grave. Le patron Newtype résout ce problème en créant des types distincts qui encapsulent des types primitifs.
Un newtype est simplement une structure tuple avec un seul champ :
struct Password(String);
struct Email(String);
Bien que tous deux encapsulent une String, le compilateur les traite comme des types complètement différents. Vous ne pouvez pas transmettre accidentellement un Password là où un Email est attendu : le compilateur détectera l'erreur.
fn send_reset_email(email: Email, password: Password) {
// ...
}
let email = Email(String::from("user@example.com"));
let pass = Password(String::from("secret123"));
send_reset_email(email, pass); // Ordre correct imposé par les types
// send_reset_email(pass, email); // Ne compilera pas !
Pour access la valeur inner, utilise .0 puisqu'il s'agit d'une structure tuple :
let pass = Password(String::from("secret123"));
println!("Length: {}", pass.0.len());
Vous pouvez également ajouter des méthodes à votre newtype, en lui donnant un comportement propre à ce qu’il représente, comme la validation ou le formatage, ce qui n’aurait pas de sens avec un simple String.
Défi
FacileConstruisons un système d’inscription d’utilisateurs sûr du point de vue des types en utilisant le modèle Newtype ! Vous allez créer des types distincts pour Username et UserId afin d’éviter de les mélanger accidentellement, même si tous deux encapsulent des valeurs simples.
Vous allez organiser votre code sur deux fichiers :
user_types.rs: Define deux structs newtype public :Usernameencapsulant unStringetUserIdencapsulant unu32. Ajoutez à chacun une method appeléevaluequi returns une référence aux données inner (pour Username, return&String; pour UserId, return&u32). Cela fournit un access contrôlé aux valeurs encapsulées.main.rs: Importez votre module user_types et créez une function appeléedisplay_userqui takes unUserIdcomme premier parameter et unUsernamecomme second parameter. La function should afficher les informations de l’user. Créez ensuite des instances des deux types à l’aide des inputs fournis et appelez votre function.
L’idée essentielle est que, même si UserId et Username sont de simples wrappers, le compilateur les traite comme des types complètement différents. Vous ne pouvez pas passer accidentellement un nom d’utilisateur là où un ID est attendu !
Votre function display_user should afficher dans ce format :
User #{id}: {username}Par exemple, avec les inputs 42 et alice_dev :
User #42: alice_devEt avec les inputs 1001 et bob_smith :
User #1001: bob_smithVous recevrez deux inputs : l’ID de l’user (à parse en u32) et la chaîne du nom d’utilisateur.
Essayez vous-même
mod user_types;
use user_types::{Username, UserId};
// TODO: Créez une fonction appelée `display_user` qui prend :
// - Premier paramètre : UserId
// - Deuxième paramètre : Username
// La fonction doit afficher : "User #{id}: {username}"
// Utilisez la méthode .value() pour accéder aux valeurs internes
fn main() {
// Lire l'entrée
let mut id_input = String::new();
std::io::stdin().read_line(&mut id_input).expect("Failed to read line");
let id: u32 = id_input.trim().parse().expect("Invalid number");
let mut username_input = String::new();
std::io::stdin().read_line(&mut username_input).expect("Failed to read line");
let username = username_input.trim().to_string();
// TODO: Créez des instances de UserId et Username en utilisant les entrées
// TODO: Appelez display_user avec les arguments corrects
}
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Object Oriented Programming
1Méthodes et comportement
Introduction aux blocs d'implémentationLe paramètre SelfMéthodes mutablesFonctions associéesBlocs d'implémentation multiplesChaînage de méthodesRécapitulatif - Actions sur le rectangle4Projet : Animal virtuel
Définir l'animalNourrir l'animal7Traits standards
Le trait DebugLe trait DisplayClone et CopyLes traits d'égalitéRécapitulatif - Point affichable10Projet : Système de documents
Le trait DrawComposant texte2Encapsulation et modules
Bases des modulesLe mot-clé publicChamps privésGettersSettersRécapitulatif - Casier sécurisé5Généricité
Structures génériquesMéthodes génériquesPlusieurs types génériquesFonctions génériquesRécapitulatif - Point de coordonnées8Les Traits comme contraintes
Syntaxe des contraintes de traitContraintes multiplesLa clause whereRetourner des types avec des traitsRécapitulatif - Afficheur générique11Design Patterns en Rust
Le pattern NewtypeCompositionLe trait DropFrom et IntoRécapitulatif - Mock de Smart Pointer3Enums avancées
Enums avec donnéesMéthodes sur les EnumsMatching des variantes de donnéesL'Enum Option revisitéeRécapitulatif - Enum Shape6Définition des Traits
Qu'est-ce qu'un Trait ?Implémenter des TraitsImplémentations par défautRedéfinir les comportements par défautTraits avec paramètresRécapitulatif - Lecteur multimédiaEntraînez-vous par vous-même : Compilateur Rust en ligne