Newtype-Pattern
Teil des Abschnitts Objektorientierte Programmierung der Rust-Journey von Coddy. Lektion 54 von 61.
Manchmal hast du zwei Werte desselben Typs, die völlig unterschiedliche Dinge darstellen. Die E-Mail-Adresse und das Passwort eines Benutzers könnten beide String-Werte sein, aber sie versehentlich in einem Funktionsaufruf zu vertauschen, wäre ein schwerwiegender Fehler. Das Newtype-Muster löst dieses Problem, indem es verschiedene Typen erstellt, die primitive Datentypen umschließen.
Ein Newtype ist einfach eine Tupelstruktur mit einem einzigen Feld:
struct Password(String);
struct Email(String);
Obwohl beide einen String umschließen, behandelt der Compiler sie als vollständig unterschiedliche Typen. Du kannst nicht versehentlich ein Password übergeben, wo ein Email erwartet wird: Der Compiler erkennt den Fehler.
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); // Korrekte Reihenfolge durch Typen erzwungen
// send_reset_email(pass, email); // Kompiliert nicht!
Um auf den inneren Wert zuzugreifen, verwende .0, da es sich um eine Tupelstruktur handelt:
let pass = Password(String::from("secret123"));
println!("Length: {}", pass.0.len());
Du kannst deinem Newtype auch Methoden hinzufügen und ihm damit ein Verhalten geben, das speziell zu dem passt, was es repräsentiert, etwa Validierung oder Formatierung, was bei einem einfachen String keinen Sinn ergeben würde.
Aufgabe
EinfachErstelle ein typsicheres Benutzerregistrierungssystem mithilfe des Newtype-Musters! Du erstellst unterschiedliche Typen für Username und UserId, um zu verhindern, dass sie versehentlich verwechselt werden, obwohl beide einfache Werte kapseln.
Du organisierst deinen Code über zwei Dateien:
user_types.rs: Definiere zwei öffentliche Newtype-Strukturen:Username, die einenStringkapselt, undUserId, die einenu32kapselt. Füge jeder Struktur eine Methode namensvaluehinzu, die eine Referenz auf die inneren Daten zurückgibt (für Username soll&Stringzurückgegeben werden, für UserId&u32). Dadurch erhältst du kontrollierten Zugriff auf die gekapselten Werte.main.rs: Binde dein user_types-Modul ein und erstelle eine Funktion namensdisplay_user, die als ersten Parameter einUserIdund als zweiten Parameter einenUsernameentgegennimmt. Die Funktion soll die Benutzerinformationen ausgeben. Erstelle anschließend Instanzen beider Typen mithilfe der bereitgestellten Eingaben und rufe deine Funktion auf.
Die entscheidende Erkenntnis hierbei ist, dass der Compiler UserId und Username als vollständig unterschiedliche Typen behandelt, obwohl sie nur einfache Wrapper sind. Du kannst nicht versehentlich einen Benutzernamen übergeben, wenn eine ID erwartet wird!
Deine display_user-Funktion soll in diesem Format ausgeben:
User #{id}: {username}Zum Beispiel mit den Eingaben 42 und alice_dev:
User #42: alice_devUnd mit den Eingaben 1001 und bob_smith:
User #1001: bob_smithDu erhältst zwei Eingaben: die Benutzer-ID (als u32 parsen) und den Benutzernamen als Zeichenkette.
Probier es selbst
mod user_types;
use user_types::{Username, UserId};
// TODO: Erstelle eine Funktion namens `display_user`, die Folgendes nimmt:
// - Erster Parameter: UserId
// - Zweiter Parameter: Username
// Die Funktion sollte ausgeben: "User #{id}: {username}"
// Verwende die .value()-Methode, um auf die inneren Werte zuzugreifen
fn main() {
// Eingabe lesen
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: Erstelle Instanzen von UserId und Username unter Verwendung der Eingaben
// TODO: Rufe display_user mit den korrekten Argumenten auf
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Methoden und Verhalten
Einführung in ImplementierungsblöckeDer Self-ParameterVeränderliche MethodenAssoziierte FunktionenMehrere ImplementierungsblöckeMethod ChainingRückblick – Rechteck-Aktionen4Projekt: Virtuelles Haustier
Das Haustier definierenDas Haustier füttern7Standard-Traits
Das Debug-TraitDas Display-TraitClone und CopyGleichheits-TraitsZusammenfassung – Druckbarer Punkt10Projekt: Dokumentensystem
Das Draw-TraitText-Komponente2Kapselung und Module
Grundlagen der ModuleDas Public-SchlüsselwortPrivate FelderGetterSetterWiederholung – Sicherer Tresor5Generics
Generische StructsGenerische MethodenMehrere generische TypenGenerische FunktionenZusammenfassung - Koordinatenpunkt8Traits als Einschränkungen
Syntax für Trait BoundsMehrfache BoundsDie Where-KlauselTypen mit Traits zurückgebenRückblick – Generischer Drucker11Design Patterns in Rust
Newtype-PatternKompositionDas Drop-TraitFrom und IntoZusammenfassung – Smart Pointer Mock3Fortgeschrittene Enums
Enums mit DatenMethoden für EnumsMatching von DatenvariantenDas Option-Enum erneut betrachtetZusammenfassung – Shape-Enum6Definition von Traits
Was ist ein Trait?Traits implementierenStandardimplementierungenStandardimplementierungen überschreibenTraits mit ParameternZusammenfassung – Media PlayerÜbe selbstständig: Online-Rust-Compiler