Fortgeschrittene Datenkapselung und API-Design mit Strukturen (Structs)
Dieses Kapitel richtet sich an Entwickler, die Rust auf professionellem Niveau einsetzen wollen. Bei größeren Codebasen reicht es nicht mehr aus, Daten einfach nur in Strukturen zusammenzufassen. Wir müssen uns fragen: Wie können wir unsere Programmier-Schnittstellen (APIs) so gestalten, dass sie robust gegen Fehlbenutzung sind, Invarianten zur Laufzeit garantieren und das mächtige Typ-System von Rust nutzen, um logische Fehler bereits zur Kompilierzeit auszuschließen?
In der Software-Architektur spricht man oft vom Prinzip “Make illegal states unrepresentable” (Mach ungültige Zustände undarstellbar). Strukturen sind in Rust das primäre Werkzeug, um dieses Prinzip in die Praxis umzusetzen.
Item 31: Kapsle Invarianten konsequent durch Sichtbarkeitsgrenzen (pub vs. private Felder)
Die Alltagsanalogie: Die Kaffeemaschine
Stellen Sie sich eine moderne Kaffeemaschine vor. Als Benutzer interagieren Sie ausschließlich mit den Knöpfen auf der Außenseite: “Espresso”, “Lungo” oder “Ausschalten”. Dies ist die öffentliche Schnittstelle (die Public API). Das Innenleben der Maschine – die Wassertemperatur, der Druck der Pumpe und die Position des Mahlwerks – ist für Sie unzugänglich hinter einem Gehäuse verborgen (Kapselung).
Würde der Hersteller das Gehäuse weglassen und Ihnen erlauben, während des Brühvorgangs direkt an den Drähten oder dem Druckventil zu drehen, könnten Sie die Maschine leicht beschädigen oder sich verletzen. Die Maschine hat eine Invariante: Der Druck während des Brühvorgangs muss exakt 9 Bar betragen, um optimalen Kaffee zu extrahieren und eine Explosion zu verhindern. Durch das Gehäuse (Sichtbarkeitsgrenze) wird diese Invariante sichergestellt.
Theorie und Konzepte
In vielen objektorientierten Sprachen wie Java oder C++ ist die Klasse die grundlegende Kapselungseinheit. In Rust hingegen ist das Modul (mod) die Kapselungseinheit. Das bedeutet:
- Eine Struktur, die in einem Modul definiert ist, hat vollen Zugriff auf alle privaten Felder aller anderen Strukturen im selben Modul.
- Code außerhalb des definierenden Moduls kann auf private Felder einer Struktur weder lesend noch schreibend zugreifen.
Wenn wir alle Felder einer Struktur mit pub versehen, geben wir jegliche Kontrolle über unsere Daten auf. Jeder externe Code kann die Felder nach Belieben verändern. Das mag für einfache Datencontainer (wie ein rein mathematischer Punkt { pub x: f64, pub y: f64 } ohne logische Invarianten) vollkommen in Ordnung sein. Sobald jedoch Logik im Spiel ist – beispielsweise, dass ein Text nicht leer sein darf, ein Wert in einem bestimmten Bereich liegen muss oder zwei Felder zueinander synchron sein müssen –, müssen die Felder privat bleiben.
Um ein solches Objekt sicher zu erzeugen, verwenden wir einen Konstruktor (konventionell eine assoziierte Funktion namens new, die manchmal ein Result\<T, E\> oder Option\<T\> zurückgibt) und kontrollieren den Zugriff über Getter- und Setter-Methoden im impl-Block.
Praxisbeispiel: Das Benutzerkonto
Wir wollen ein Benutzerkonto modellieren. Unsere Invarianten lauten:
- Der Benutzername darf nicht leer sein.
- Der Aktivierungsstatus und die Bonuspunkte müssen kontrolliert verändert werden. Bonuspunkte dürfen niemals negativ sein.
Schlechter Stil (Alle Felder öffentlich):
#![allow(unused)]
fn main() {
// In einem externen Modul oder einer anderen Datei
pub struct Benutzerkonto {
pub name: String,
pub punkte: i32,
pub aktiv: bool,
}
}
Bei dieser Struktur kann ein externer Aufrufer problemlos folgenden Code schreiben:
#![allow(unused)]
fn main() {
let mut konto = Benutzerkonto {
name: String::new(), // Invariante verletzt: leerer Name!
punkte: -999, // Invariante verletzt: negative Punkte!
aktiv: true,
};
konto.punkte = -5000; // Beliebige Manipulation zur Laufzeit möglich!
}
Es gibt keine Möglichkeit, diesen Missbrauch zur Laufzeit oder durch die Struktur selbst zu verhindern.
Guter Stil (Kapselung durch Sichtbarkeitsgrenzen):
Wir verschieben die Struktur in ein eigenes Modul (oder betrachten sie aus Sicht eines externen Moduls) und machen die Felder privat.
pub mod benutzer {
/// Ein Benutzerkonto mit gekapselten Invarianten.
#[derive(Debug)]
pub struct Benutzerkonto {
name: String, // Privat! Kein `pub` davor.
punkte: u32, // Privat! Verhindert negative Werte durch vorzeichenlosen Typ `u32`.
aktiv: bool, // Privat!
}
impl Benutzerkonto {
/// Der Konstruktor erzwingt die Invarianten bei der Erstellung.
/// Gibt `Result`, da die Erstellung bei ungültigen Eingaben scheitern kann.
pub fn new(name: &str) -> Result<Self, &'static str> {
if name.trim().is_empty() {
return Err("Der Benutzername darf nicht leer sein.");
}
Ok(Self {
name: name.to_string(),
punkte: 0, // Standardmäßig startet jeder Benutzer mit 0 Punkten
aktiv: true,
})
}
/// Ein kontrollierter "Getter" für den Namen.
/// Gibt eine Referenz zurück, um ein Kopieren des Strings zu vermeiden.
pub fn name(&self) -> &str {
&self.name
}
/// Ein Getter für die Punkte.
pub fn punkte(&self) -> u32 {
self.punkte
}
/// Eine kontrollierte Methode zur Erhöhung der Punkte (Invariante bleibt geschützt).
pub fn punkte_hinzufuegen(&mut self, wert: u32) {
self.punkte = self.punkte.saturating_add(wert);
}
/// Kontrolliertes Deaktivieren des Kontos.
pub fn deaktivieren(&mut self) {
self.aktiv = false;
}
/// Getter für den Aktivierungsstatus.
pub fn ist_aktiv(&self) -> bool {
self.aktiv
}
}
}
fn main() {
// Versuch, ein ungültiges Konto anzulegen:
let fehlgeschlagen = benutzer::Benutzerkonto::new(" ");
assert!(fehlgeschlagen.is_err());
println!("Erstellung blockiert: {:?}", fehlgeschlagen.err().unwrap());
// Erfolgreiche Erstellung:
let mut konto = benutzer::Benutzerkonto::new("Thorsten").unwrap();
println!("Konto erfolgreich erstellt für: {}", konto.name());
// Punkte hinzufügen über die kontrollierte Schnittstelle:
konto.punkte_hinzufuegen(150);
println!("Aktuelle Punkte: {}", konto.punkte());
// Folgender Code würde zu einem Compilerfehler führen, da die Felder privat sind:
// konto.name = String::new(); // Fehler: field `name` of struct `Benutzerkonto` is private
// konto.punkte = 100; // Fehler: field `punkte` is private
}
Zeilenweise Erklärung des Codes:
- Zeile 4-6: Die Felder
name,punkteundaktivhaben kein vorangestelltespub. Sie sind somit außerhalb des Modulsbenutzerunsichtbar und unveränderbar. - Zeile 10:
pub fn new(...) -> Result\<Self, &'static str\>: Dies ist die einzige Möglichkeit, eine Instanz vonBenutzerkontoaußerhalb des Moduls zu erstellen. Sie gibt einResultzurück, um dem Aufrufer mitzuteilen, ob die Erstellung erfolgreich war. - Zeile 11-13: Hier wird die Invariante geprüft. Wenn der Name leer ist, bricht die Funktion sofort ab und gibt einen Fehler zurück. Es ist unmöglich, eine Instanz mit leerem Namen zu erhalten.
- Zeile 24:
pub fn name(&self) -> &str: Ein typischer Rust-Getter. Statt denStringper Move zu übergeben (was das Struct zerstört würde), leihen wir uns den Inhalt als temporäre Referenz (&str) aus. - Zeile 34:
self.punkte.saturating_add(wert): Verhindert einen arithmetischen Überlauf (Overflow) zur Laufzeit. Sollte die maximale Zahl überschritten werden, verbleibt der Wert beim Maximum vonu32.
Item 32: Nutze das Newtype-Pattern zur Absicherung von Typsicherheit auf API-Ebene
Die Alltagsanalogie: Der Tankstellen-Unfall
Stellen Sie sich vor, Sie fahren an eine Tankstelle. Sie haben einen Benzinkanister und einen Dieselkanister. Beide Kanister bestehen aus dem gleichen Material (Kunststoff) und fassen beide exakt 10 Liter (primitiver Datentyp f64). Wenn Sie die Kanister nicht beschriften, ist es extrem leicht, sie zu verwechseln und Diesel in ein Benzinauto zu füllen – mit katastrophalen Folgen für den Motor.
Das Newtype-Pattern ist der physikalische Schutz: Es ist so, als hätte der Benzin-Einfüllstutzen eine völlig andere Form als der Diesel-Einfüllstutzen. Selbst wenn Sie blind versuchen, den falschen Treibstoff einzufüllen, scheitern Sie mechanisch. Der Compiler ist in diesem Fall der Einfüllstutzen, der den Fehler verhindert.
Theorie und Konzepte
Viele Programmierer neigen zur sogenannten “Primitive Obsession” (Primitiven-Besessenheit). Sie verwenden grundlegende Datentypen wie i32, f64 oder String für fachlich völlig unterschiedliche Konzepte.
Beispiel:
#![allow(unused)]
fn main() {
fn berechne_geschwindigkeit(strecke: f64, zeit: f64) -> f64 {
strecke / zeit
}
}
Hier kann man beim Aufruf kinderleicht zeit und strecke vertauschen: berechne_geschwindigkeit(10.0, 120.0) statt berechne_geschwindigkeit(120.0, 10.0). Da beide Parameter f64 sind, merkt der Compiler nichts.
Das Newtype-Pattern löst dies, indem es den primitiven Typ in eine einwertige Tuple-Struktur (ein sogenanntes Tuple Struct) einpackt:
#![allow(unused)]
fn main() {
struct Meter(f64);
struct Sekunden(f64);
}
Zur Laufzeit gibt es hierbei keinen Performance-Overhead. Der Rust-Compiler optimiert die umschließende Struktur komplett weg, sodass im Maschinencode nur noch die reine Fließkommazahl steht (Zero-Cost Abstraction). Doch zur Kompilierzeit sind Meter und Sekunden zwei völlig inkompatible Typen.
Praxisbeispiel: Celsius vs. Fahrenheit
Wir wollen Temperatur-Berechnungen durchführen und verhindern, dass Celsius- und Fahrenheit-Werte versehentlich vertauscht oder miteinander addiert werden.
use std::ops::Add;
/// Eine Temperatur in Grad Celsius.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64); // Das innere Feld ist öffentlich lesbar, aber als Typ isoliert.
/// Eine Temperatur in Grad Fahrenheit.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);
impl Celsius {
/// Konvertiert Celsius in Fahrenheit.
pub fn to_fahrenheit(self) -> Fahrenheit {
Fahrenheit(self.0 * 1.8 + 32.0)
}
}
impl Fahrenheit {
/// Konvertiert Fahrenheit in Celsius.
pub fn to_celsius(self) -> Celsius {
Celsius((self.0 - 32.0) / 1.8)
}
}
// Wir können Traits wie `Add` implementieren, um das Rechnen komfortabel zu machen.
impl Add for Celsius {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
Celsius(self.0 + other.0)
}
}
/// Diese Funktion akzeptiert ausschließlich Celsius.
/// Es ist unmöglich, ihr versehentlich Fahrenheit zu übergeben!
pub fn pruefe_hitzewarnung(temp: Celsius) {
if temp.0 >= 38.0 {
println!("WARNUNG: Extreme Hitze! ({:.1}°C)", temp.0);
} else {
println!("Temperatur im normalen Bereich ({:.1}°C)", temp.0);
}
}
fn main() {
let t_celsius = Celsius(36.5);
let t_fahrenheit = Fahrenheit(100.0);
// Wir können Celsius-Werte miteinander addieren:
let waermer = t_celsius + Celsius(2.0);
pruefe_hitzewarnung(waermer);
// Folgender Code führt zu einem klaren Compilerfehler:
// pruefe_hitzewarnung(t_fahrenheit);
// Fehler: expected `Celsius`, found `Fahrenheit`
// Richtige Vorgehensweise: Explizite Konvertierung aufrufen
let konvertiert = t_fahrenheit.to_celsius();
pruefe_hitzewarnung(konvertiert);
}
Zeilenweise Erklärung des Codes:
- Zeile 5 & 9:
pub struct Celsius(pub f64);definiert das Tuple-Struct. Daspub f64im Inneren erlaubt es dem Aufrufer, über.0direkt auf den Wert zuzugreifen. Möchte man das verhindern, lässt man das innerepubweg und stellt stattdessen eine Methode.value()bereit. - Zeile 13-15:
to_fahrenheitkonsumiertself(was billig ist, daCelsiusdasCopy-Trait implementiert) und gibt die umgerechnete StrukturFahrenheitzurück. - Zeile 25:
impl Add for Celsiuserlaubt die Verwendung des+-Operators für Celsius-Strukturen untereinander. Rust verbietet es jedoch standardmäßig,Celsius + Fahrenheitzu rechnen, da für diese Kombination keinAdd-Trait implementiert ist.
Item 33: Beherrsche das Typ-Zustands-Pattern (Type State Pattern) für compile-time verifizierte Zustandsmaschinen
Die Alltagsanalogie: Der Briefversand
Denken Sie an den Lebenszyklus eines physischen Briefes:
- Entwurf: Sie schreiben den Text auf ein Blatt Papier. In diesem Zustand können Sie den Text noch beliebig ändern und korrigieren.
- Versiegelt: Sie legen das Blatt in einen Umschlag und kleben ihn zu. Jetzt können Sie den Inhalt nicht mehr ändern, ohne den Umschlag zu zerstören. Der Brief ist bereit für den Versand.
- Gesendet: Der Brief befindet sich im Postkasten oder im Transit. Sie können ihn nicht mehr zurückholen, nicht mehr ändern und auch nicht noch einmal versiegeln.
Wenn ein Softwaresystem diesen Ablauf abbildet, prüfen klassische Programme zur Laufzeit: if status == Status::Gesendet { panic!("Fehler: Gesendete Briefe dürfen nicht geändert werden!"); }.
Das Typ-Zustands-Pattern verlegt diese Prüfung in die Kompilierzeit. Es sorgt dafür, dass die Methode aendern() auf einem gesendeten Brief gar nicht erst existiert.
Theorie und Konzepte
Zustandsmaschinen (State Machines) sind allgegenwärtig. Traditionell speichert man den Zustand in einem Enum-Feld innerhalb einer Struktur:
#![allow(unused)]
fn main() {
enum Status { Entwurf, Versiegelt, Gesendet }
struct Brief { inhalt: String, status: Status }
}
Das Problem dabei: Jede Methode auf Brief muss zur Laufzeit prüfen, in welchem Zustand sich das Objekt befindet. Vergisst man eine solche Prüfung, entstehen logische Programmierfehler.
Beim Type State Pattern (Typ-Zustands-Pattern) repräsentieren wir jeden Zustand durch einen eigenen Typ (meist ein leeres Unit-Struct). Die eigentliche Struktur wird über Generics mit diesem Zustand verknüpft.
Um zu verhindern, dass Rust den Zustand als Feld im Speicher anlegt (was ungenutzten Platz kosten würde), nutzen wir std::marker::PhantomData\<T\>. Dies ist ein spezieller Typ mit einer Größe von 0 Bytes, der dem Compiler signalisiert: “Diese Struktur verhält sich so, als ob sie einen Wert vom Typ T besitzt, obwohl zur Laufzeit nichts davon existiert.”
Durch die Definition von Methoden in spezifischen impl-Blöcken wie impl Brief\<Entwurf\> legen wir fest, dass bestimmte Aktionen nur in exakt diesem Zustand erlaubt sind. Ein Zustandsübergang wird vollzogen, indem die Methode das alte Objekt konsumiert (self) und ein neues Objekt mit dem neuen Zustandstyp zurückgibt.
Praxisbeispiel: Die E-Mail-Pipeline
use std::marker::PhantomData;
// 1. Wir definieren die Zustände als leere Strukturen.
// Sie dienen ausschließlich als Markierungen für den Compiler.
#[derive(Debug)]
pub struct Entwurf;
#[derive(Debug)]
pub struct Bereit;
#[derive(Debug)]
pub struct Gesendet;
// 2. Die Hauptstruktur ist generisch über den Zustand `State`.
#[derive(Debug)]
pub struct Email<State> {
empfaenger: String,
inhalt: String,
// PhantomData teilt dem Compiler mit, dass `State` logisch genutzt wird,
// belegt aber zur Laufzeit 0 Byte Speicherplatz.
zustand: PhantomData<State>,
}
// 3. Methoden, die in JEDEM Zustand verfügbar sein sollen.
impl<State> Email<State> {
pub fn empfaenger(&self) -> &str {
&self.empfaenger
}
}
// 4. Methoden, die NUR im Zustand `Entwurf` existieren.
impl Email<Entwurf> {
/// Konstruktor startet immer als Entwurf.
pub fn neu(empfaenger: &str) -> Self {
Self {
empfaenger: empfaenger.to_string(),
inhalt: String::new(),
zustand: PhantomData,
}
}
/// Im Entwurf darf der Inhalt editiert werden.
pub fn inhalt_schreiben(&mut self, text: &str) {
self.inhalt.push_str(text);
}
/// Der Übergang von `Entwurf` zu `Bereit`.
/// Wir konsumieren `self` (Move-Semantik) und geben einen neuen Typ zurück.
pub fn vorbereiten(self) -> Email<Bereit> {
Email {
empfaenger: self.empfaenger,
inhalt: self.inhalt,
zustand: PhantomData, // Zustand wechselt im Typ-System!
}
}
}
// 5. Methoden, die NUR im Zustand `Bereit` existieren.
impl Email<Bereit> {
/// Der Übergang von `Bereit` zu `Gesendet`.
/// Auch hier wird das alte Objekt durch `self` unbrauchbar gemacht.
pub fn senden(self) -> Email<Gesendet> {
println!("Sende E-Mail an {}...", self.empfaenger);
println!("Inhalt: \"{}\"", self.inhalt);
Email {
empfaenger: self.empfaenger,
inhalt: self.inhalt,
zustand: PhantomData,
}
}
}
// 6. Im Zustand `Gesendet` gibt es keine verändernden Methoden mehr.
// Die E-Mail is "eingefroren".
fn main() {
// Phase 1: Entwurf erstellen und schreiben
let mut email = Email::neu("thorsten@example.com");
email.inhalt_schreiben("Hallo Thorsten, willkommen in Rust!");
// Folgender Code würde nicht kompilieren:
// email.senden(); // Fehler: no method named `senden` found for struct `Email<Entwurf>`
// Phase 2: E-Mail für den Versand vorbereiten
// Die alte Variable `email` ist danach nicht mehr nutzbar.
let email_bereit = email.vorbereiten();
// Folgender Code würde nicht kompilieren:
// email_bereit.inhalt_schreiben("Noch ein Text...");
// Fehler: no method named `inhalt_schreiben` found for struct `Email<Bereit>`
// Phase 3: E-Mail senden
let _email_gesendet = email_bereit.senden();
// Die E-Mail ist nun im Endzustand. Es können keine ungültigen Aktionen mehr ausgeführt werden.
}
Zeilenweise Erklärung des Codes:
- Zeile 17:
pub struct Email\<State\>deklariert die Struktur mit dem TypparameterState. Dieser Parameter bestimmt, in welchem Zustand sich die E-Mail befindet. - Zeile 21:
zustand: PhantomData\<State\>bindet den Typparameter an die Struktur. Ohne dieses Feld würde der Compiler sich beschweren: parameterStateis never used. - Zeile 25-29:
impl\<State\> Email\<State\>zeigt, wie man Methoden schreibt, die für alle Zustände gleichermaßen gelten. Hier kann man unabhängig vom aktuellen Zustand den Empfänger abfragen. - Zeile 32:
impl Email\<Entwurf\>schränkt alle folgenden Methoden auf E-Mails im ZustandEntwurfein. - Zeile 47:
pub fn vorbereiten(self) -> Email\<Bereit\>nimmtselfper Ownership (Wertübergabe) entgegen. Dadurch wird die ursprünglicheEmail\<Entwurf\>-Instanz im Aufrufer zerstört bzw. ungültig gemacht. Zurückgegeben wird eine frisch konstruierteEmail\<Bereit\>. Das verhindert, dass man mit der alten Entwurfs-Instanz weiterarbeitet.
Die Struktur-Update-Syntax (..) und ihre Move-Semantik
Rust bietet eine sehr elegante Möglichkeit, eine neue Instanz einer Struktur zu erstellen, indem man die Werte einer bereits existierenden Instanz kopiert oder verschiebt. Dies geschieht mithilfe der Struktur-Update-Syntax (..).
#![allow(unused)]
fn main() {
struct Benutzer {
id: u64,
name: String,
aktiv: bool,
}
}
Wenn wir nun einen neuen Benutzer erstellen wollen, der dieselben Daten wie ein bestehender Benutzer hat, aber mit einer neuen ID, schreiben wir:
#![allow(unused)]
fn main() {
let benutzer1 = Benutzer {
id: 1,
name: String::from("Thorsten"),
aktiv: true,
};
let benutzer2 = Benutzer {
id: 2,
..benutzer1 // Alle anderen Felder aus benutzer1 übernehmen
};
}
Die Move-Semantik bei nicht-Copy-Feldern
Was auf den ersten Blick wie ein bequemes Kopieren aussieht, birgt ein wichtiges Detail bezüglich Rusts Speichersicherheits-Modell: Die Move-Semantik.
Wenn der Compiler die Zeile ..benutzer1 verarbeitet, verhält er sich so, als ob die Felder einzeln zugewiesen würden:
#![allow(unused)]
fn main() {
let benutzer2 = Benutzer {
id: 2,
name: benutzer1.name, // String wird VERSCHOBEN (Move)!
aktiv: benutzer1.aktiv, // bool wird KOPIERT (Copy)!
};
}
Da das Feld name vom Typ String ist und String nicht das Copy-Trait implementiert (weil es Heap-Speicher verwaltet), wird der Besitz (Ownership) des Strings von benutzer1 auf benutzer2 übertragen.
Das hat fundamentale Auswirkungen auf die Gültigkeit von benutzer1:
- Teilweise Verschiebung (Partial Move): Da
benutzer1.namewegbewegt wurde, ist die Strukturbenutzer1als Ganzes ab diesem Zeitpunkt ungültig und zerstört. - Sie können
benutzer1nicht mehr als Funktionsargument übergeben oder ausgeben. - Der Zugriff auf unbeschädigte Felder wie
benutzer1.id(das einu64is und somit kopiert wurde) wäre theoretisch noch erlaubt, ist aber in der Praxis unidiomatisch und wird vom Compiler streng überwacht.
Visualisierung des Speicherzustands nach dem Update:
Vor dem Update:
benutzer1 [ id: 1, name: "Thorsten" (Zeiger auf Heap), aktiv: true ]
│
└───► [T][h][o][r][s][t][e][n] (Heap-Speicher)
Nach dem Update:
benutzer1 [ id: 1, name: UNGÜLTIG (Verschoben!), aktiv: true ]
benutzer2 [ id: 2, name: ──────────────────────────────────────────┐
▼
[T][h][o][r][s][t][e][n]
Der Compilerfehler im Detail
Lass uns ansehen, was passiert, wenn wir versuchen, benutzer1 nach dem Update weiterzuverwenden:
struct Benutzer {
id: u64,
name: String,
aktiv: bool,
}
fn main() {
let benutzer1 = Benutzer {
id: 1,
name: String::from("Thorsten"),
aktiv: true,
};
let benutzer2 = Benutzer {
id: 2,
..benutzer1
};
// Dieser Aufruf führt zu einem Compilerfehler!
println!("Benutzer 1 Name: {}", benutzer1.name);
}
Wenn Sie versuchen, diesen Code zu kompilieren, bricht der Compiler mit folgender Meldung ab:
error[E0382]: borrow of partially moved value: `benutzer1`
--> src/main.rs:20:38
|
15 | ..benutzer1
| --------- value moved here
...
20 | println!("Benutzer 1 Name: {}", benutzer1.name);
| ^^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `benutzer1.name` has type `String`, which does not implement the `Copy` trait
Wie man den Fehler behebt
Sollte die ursprüngliche Instanz nach dem Update weiterhin benötigt werden, haben Sie zwei Möglichkeiten:
-
Explizites Klonen der nicht-
Copy-Felder: Sie überlassen das Feld nicht der automatischen Update-Syntax, sondern klonen es manuell. Dadurch bleibt der Besitz bei der alten Struktur erhalten.#![allow(unused)] fn main() { let benutzer2 = Benutzer { id: 2, name: benutzer1.name.clone(), // Klon erzeugen, Original behalten ..benutzer1 // Kopiert nun nur noch das `aktiv`-Feld (das Copy ist) }; // Jetzt sind sowohl benutzer1 als auch benutzer2 voll einsatzbereit! println!("B1: {}, B2: {}", benutzer1.name, benutzer2.name); } -
Implementierung des
Clone-Traits für die gesamte Struktur: Wenn Sie die gesamte Struktur klonen können, können Sie zuerst ein Duplikat erstellen und dieses verändern.#![allow(unused)] fn main() { #[derive(Clone)] struct Benutzer { id: u64, name: String, aktiv: bool, } let benutzer2 = Benutzer { id: 2, ..benutzer1.clone() // Klon der gesamten Struktur als Basis nutzen }; }
Zusammenfassung und Best Practices für Strukturen
- Geheimnisprinzip wahren: Deklarieren Sie Felder standardmäßig immer als privat. Machen Sie Felder nur dann öffentlich (
pub), wenn es sich um reine, invariantenfreie Datenbehälter handelt. - Typen statt Fehlerprüfungen: Verwenden Sie das Newtype-Pattern, um Verwechslungen von physikalischen Einheiten, Datenbank-IDs oder Währungen bereits beim Kompilieren unmöglich zu machen.
- Zustände über Typen sichern: Nutzen Sie das Typ-Zustands-Pattern mit
PhantomData\<T\>, um sicherzustellen, dass Methoden nur aufgerufen werden können, wenn sich das Objekt im logisch korrekten Zustand befindet. - Vorsicht bei
..: Denken Sie daran, dass die Struct-Update-Syntax Ownership transferiert, wenn Felder nichtCopyimplementieren. Nutzen Sie.clone(), falls die Quellstruktur intakt bleiben muss.