Kapitel 12: Enumerationen (Enums) im Detail – Die Magie der Vielfalt
Stell dir vor, du spielst ein Abenteuerspiel am Computer. Bevor die Reise losgeht, stehst du vor einer wichtigen Entscheidung: Welcher Charakterklasse soll dein Held angehören? Du kannst als starker Krieger mit einem mächtigen Breitschwert antreten, als weiser Magier mit einem glühenden Zauberstab oder als flinker Dieb, der sich lautlos durch die Schatten bewegt.
Eines ist dabei klar: Dein Charakter kann zu jedem Zeitpunkt immer nur genau eine dieser Klassen haben. Du kannst nicht gleichzeitig ein Magier und ein Krieger sein.
Um genau solche Situationen im Programmiercode abzubilden, besitzt die Programmiersprache Rust ein unglaublich mächtiges Werkzeug: Enumerationen, kurz Enums (gesprochen wie das englische Wort „ih-nams“). Das deutsche Wort dafür lautet Aufzählungen.
In diesem Kapitel werden wir Schritt für Schritt und ohne kompliziertes Fachchinesisch lernen, was Enums sind, warum sie dein Programmiererleben sicherer machen und wie du sie in deinen eigenen Abenteuern – äh, Programmen – einsetzt!
1. Lernziele – Das wirst du heute lernen
- Was ein Enum is: Du verstehst das Konzept von Enums anhand einfacher Alltagsbeispiele.
- Die Syntax von Enums: Du lernst, wie man Enums in Rust schreibt und erstellt.
- Enums mit eigenen Werten: Du erfährst, wie jede Variante eines Enums unterschiedliche Zusatzinformationen (Daten) transportieren kann.
- Der Münzsortierer (
match): Du lernst, wie du mithilfe vonmatchauf die verschiedenen Varianten reagierst und ihre Daten auspackst. - Sicherheit durch den Compiler: Du verstehst, warum Rust dich zwingt, immer alle Möglichkeiten zu bedenken, und wie das Fehler verhindert.
- Typische Compilerfehler: Du lernst typische Stolpersteine kennen und erfährst, wie du sie ganz leicht aus dem Weg räumst.
2. Alltagsanalogien für Enums
Bevor wir Code schreiben, lass uns zwei Bilder im Kopf verankern, damit du sofort verstehst, was Enums tun.
Analogie 1: Das Auswahlmenü im Rollenspiel
Stell dir eine Ampel vor. Eine Ampel kann rot, gelb oder grün sein. Sie kann niemals rot-grün gestreift sein oder alle drei Farben gleichzeitig anzeigen (zumindest nicht, wenn sie richtig funktioniert!).
Genauso verhält es sich mit unserem Rollenspiel-Charakter. Wir haben drei feste Möglichkeiten:
- Krieger
- Magier
- Dieb
Ein Enum ist wie ein Steckplatz oder ein Auswahlmenü. Du definierst eine Liste von erlaubten Möglichkeiten (wir nennen sie Varianten). Wenn du später eine Variable mit diesem Enum erstellst, muss sie sich für genau eine dieser Varianten entscheiden.
Analogie 2: Der Münzsortierer oder Weichensteller (match)
Wenn du ein Enum hast, möchtest du natürlich auch im Code unterschiedlich darauf reagieren. Wenn der Spieler ein Krieger ist, greift er mit dem Schwert an; wenn er ein Magier ist, spricht er einen Zauberspruch.
Das funktioniert in Rust wie ein Münzsortierer:
Stell dir eine Spardose vor, in die du Münzen wirfst. Im Inneren gibt es verschiedene Schlitze. Eine 1-Euro-Münze fällt durch den einen Schlitz, eine 2-Euro-Münze durch einen anderen. Jede Münze nimmt automatisch den Pfad, der genau zu ihrer Größe passt.
In Rust übernimmt das Schlüsselwort match diese Aufgabe. Es schaut sich das Enum an, stellt die Weiche und leitet das Programm in den exakt passenden Pfad um.
3. Die einfachste Form eines Enums in Rust
Fangen wir ganz simpel an. Wir erstellen ein Enum für unsere Charakterklassen.
#![allow(unused)]
fn main() {
// Hier definieren wir unser erstes Enum!
// Es heißt "CharakterKlasse" und bietet drei feste Auswahlmöglichkeiten.
enum CharakterKlasseSimple {
Krieger,
Magier,
Dieb,
}
}
Wie erstellen wir nun einen Charakter im Code?
Um eine Variable mit einer bestimmten Variante zu erstellen, benutzen wir den Namen des Enums, gefolgt von zwei Doppelpunkten (::) und der gewünschten Variante:
fn main() {
// Thorsten wählt den Pfad der Weisheit und wird ein Magier!
let helden_klasse = CharakterKlasseSimple::Magier;
// Wenn wir einen Dieb erstellen wollen:
let schatten_klasse = CharakterKlasseSimple::Dieb;
}
Wichtig für Einsteiger: Siehst du die Schreibweise? Wir benutzen für den Namen des Enums die sogenannte PascalCase-Schreibweise (jedes Wort beginnt mit einem Großbuchstaben, z. B.
CharakterKlasseSimple). Die Varianten selbst schreiben wir ebenfalls groß (Krieger,Magier,Dieb). Das hilft uns, im Code sofort zu erkennen, dass es sich um ein Enum handelt!
4. Enums mit Werten (Daten transportieren)
Jetzt kommt das absolute Super-Feature von Rust. In vielen anderen Programmiersprachen sind Enums nur einfache Listen von Wörtern (oder heimlichen Zahlen). In Rust hingegen kann jede Variante eines Enums ihre ganz eigenen Daten mit sich herumtragen!
Gehen wir zurück zu unserem Spiel:
- Ein Krieger schleppt ein schweres Schwert mit sich herum. Für uns ist wichtig zu wissen: Wie viel Kilogramm wiegt dieses Schwert?
- Ein Magier besitzt einen Zauberstab. Hier müssen wir wissen: Wie stark ist die magische Kraft dieses Stabes?
- Ein Dieb ist minimalistisch unterwegs. Er braucht keine extra Ausrüstungsinformationen in seinem Enum, er verlässt sich auf seine flinken Hände.
Wir können unser Enum nun so erweitern, dass jede Variante genau die Daten speichert, die sie benötigt:
#![allow(unused)]
fn main() {
// Wir definieren ein Enum, bei dem die Varianten eigene Datenfelder besitzen!
enum CharakterKlasse {
// Ein Krieger hat ein Schwert mit einem Gewicht als Fließkommazahl (f64)
Krieger { schwert_gewicht_kg: f64 },
// Ein Magier hat einen Zauberstab mit einer bestimmten Kraft als Ganzzahl (u32)
Magier { zauberstab_kraft: u32 },
// Ein Dieb hat in diesem einfachen Beispiel keine zusätzlichen Daten
Dieb,
}
}
Wie befüllen wir diese Varianten mit Leben?
Wenn wir jetzt einen Charakter erstellen, müssen wir die Daten direkt mitliefern. Das sieht fast so aus, als würden wir ein normales Struct (eine Struktur) erstellen:
fn main() {
// Wir erstellen Thorsten, den Magier, dessen Zauberstab die Stärke 150 hat
let thorsten = CharakterKlasse::Magier { zauberstab_kraft: 150 };
// Wir erstellen Erik, den Krieger, mit einem 4.5 kg schweren Schwert
let erik = CharakterKlasse::Krieger { schwert_gewicht_kg: 4.5 };
// Und Sonja, die Diebin, die ohne schwere Lasten reist
let sonja = CharakterKlasse::Dieb;
}
Warum ist das so genial für die Speichersicherheit?
Stell dir vor, wir hätten stattdessen ein einziges großes Struct für alle Charaktere gebaut, das so aussieht:
#![allow(unused)]
fn main() {
// VORSICHT: So machen wir es NICHT! Das ist unsicher und verschwendet Platz.
struct SchlechterSpieler {
klasse: String, // z.B. "Magier"
schwert_gewicht: f64, // Eigentlich nur für Krieger wichtig...
zauberstab_kraft: u32, // Eigentlich nur für Magier wichtig...
}
}
Wenn wir das so bauen, könnte jemand aus Versehen einen Charakter erstellen, bei dem die Klasse "Magier" eingetragen ist, der aber gleichzeitig ein schwert_gewicht von 10 Kilo eingetragen hat und eine zauberstab_kraft von 0. Das ergibt keinen Sinn! Außerdem verschwenden wir Speicherplatz, weil für jeden Magier leere Felder für das Schwertgewicht reserviert werden müssen.
Mit dem Rust-Enum is es physisch unmöglich, einen Magier mit Schwertgewichts-Daten zu erstellen. Der Compiler lässt das gar nicht erst zu! Das sorgt für absolute Speichersicherheit und logische Klarheit.
5. Der Weichensteller match in Aktion
Nun wollen wir unseren Charakteren eine Stimme geben. Wir möchten eine Funktion schreiben, die uns beschreibt, was für einen Helden wir vor uns haben. Hier kommt der Münzsortierer match zum Einsatz!
Lies dir den folgenden Code genau durch. Keine Sorge, darunter erkläre ich dir jede einzelne Zeile ganz genau!
#![allow(unused)]
fn main() {
// Eine Funktion, die eine Referenz auf unsere CharakterKlasse entgegennimmt
fn beschreibe_charakter(klasse: &CharakterKlasse) {
match klasse {
// Weiche 1: Wenn es sich um einen Krieger handelt, packen wir das Gewicht aus
CharakterKlasse::Krieger { schwert_gewicht_kg } => {
println!(
"Ein tapferer Krieger betritt den Raum! Sein Schwert wiegt stolze {} kg.",
schwert_gewicht_kg
);
}
// Weiche 2: Wenn es sich um einen Magier handelt, packen wir die Kraft aus
CharakterKlasse::Magier { zauberstab_kraft } => {
println!(
"Ein weiser Magier nähert sich. Sein Zauberstab knistert mit der Stärke {}!",
zauberstab_kraft
);
}
// Weiche 3: Wenn es sich um einen Dieb handelt, gibt es keine Daten zum Auspacken
CharakterKlasse::Dieb => {
println!("Lautlos huscht ein Dieb vorbei. Man hört fast nichts...");
}
}
}
}
Zeilenweise Erklärung – Was passiert hier genau?
fn beschreibe_charakter(klasse: &CharakterKlasse)- Wir definieren eine Funktion. Sie bekommt einen Parameter namens
klasse. - Das
&vorCharakterKlassebedeutet, dass wir die Daten nur ausleihen (Borrowing). Wir wollen den Charakter ja nur beschreiben und ihn nicht dabei zerstören oder besitzen!
- Wir definieren eine Funktion. Sie bekommt einen Parameter namens
match klasse { ... }- Das ist unser Münzsortierer. Rust schaut sich an, was in
klassesteckt.
- Das ist unser Münzsortierer. Rust schaut sich an, was in
CharakterKlasse::Krieger { schwert_gewicht_kg } => { ... }- Rust prüft: Ist der Charakter ein Krieger? Wenn ja, biegen wir in diesen Zweig ab.
- Der Clou: In den geschweiften Klammern
{ schwert_gewicht_kg }erschaffen wir eine neue, temporäre Variable. Rust holt den Wert aus dem Inneren des Enums heraus und legt ihn in diese Variable. Wir nennen das Pattern Matching (Musterabgleich) mit Variablenbindung. - Mit
println!(...)geben wir den Text auf dem Bildschirm aus und setzen den Wert ein.
CharakterKlasse::Magier { zauberstab_kraft } => { ... }- Ist der Charakter stattdessen ein Magier? Dann biegen wir hier ab. Rust holt die
zauberstab_kraftaus dem Enum und stellt sie uns im folgenden Codeblock zur Verfügung.
- Ist der Charakter stattdessen ein Magier? Dann biegen wir hier ab. Rust holt die
CharakterKlasse::Dieb => { ... }- Ist es ein Dieb? Da der Dieb keine extra Daten hat, schreiben wir einfach nur
CharakterKlasse::Diebauf die linke Seite und reagieren entsprechend darauf.
- Ist es ein Dieb? Da der Dieb keine extra Daten hat, schreiben wir einfach nur
6. Vollständigkeit: Warum Rust so streng wacht
Stell dir vor, du erweiterst dein Spiel nach ein paar Wochen. Du fügst eine neue Charakterklasse hinzu: den Waldläufer mit einem Bogen.
#![allow(unused)]
fn main() {
// Wir erweitern das Enum um eine vierte Variante!
enum ErweiterterCharakter {
Krieger { schwert_gewicht_kg: f64 },
Magier { zauberstab_kraft: u32 },
Dieb,
Waldlaeufer { pfeil_anzahl: u32 }, // NEU!
}
}
Wenn du in einer Sprache wie JavaScript oder Python vergisst, an allen Stellen im Code die neue Klasse einzubauen, stürzt dein Programm im schlimmsten Fall mitten im Spiel ab, wenn ein Spieler einen Waldläufer auswählt.
Nicht so in Rust!
Wenn du ein match über ein Enum schreibst, verlangt der Compiler absolute Vollständigkeit (engl. exhaustiveness). Das bedeutet: Du musst für jede einzelne Variante des Enums eine Antwort parat haben. Vergisst du auch nur eine einzige Variante, weigert sich Rust, das Programm zu kompilieren!
Das ist wie ein aufmerksamer Lehrer, der deine Hausaufgaben kontrolliert und sagt: „Du hast den Waldläufer vergessen aufzulisten. Setz dich noch mal hin und korrigiere das, bevor du spielen darfst.“
7. Typische Compilerfehler und wie du sie behebst
Damit du im Alltag keine Angst vor Compilerfehlern hast, schauen wir uns jetzt zwei typische Fehler an, auf die du garantiert stoßen wirst, und wie wir sie lösen.
Fehler 1: Die nicht-abgedeckte Variante (Non-exhaustive match)
Nehmen wir an, wir schreiben folgenden Code:
#![allow(unused)]
fn main() {
// Wir haben unser einfaches Enum von oben
enum CharakterKlasseSimple {
Krieger,
Magier,
Dieb,
}
fn spiele_sound_ab(klasse: CharakterKlasseSimple) {
match klasse {
CharakterKlasseSimple::Krieger => println!("Klirr! Schwert gezogen."),
CharakterKlasseSimple::Magier => println!("Zisch! Feuerball bereit."),
// Oh nein! Wir haben den Dieb vergessen!
}
}
}
Wenn du versuchst, diesen Code zu kompilieren, wird dich der Rust-Compiler mit einer Fehlermeldung stoppen:
error[E0004]: non-exhaustive patterns: `Dieb` not covered
--> src/main.rs:9:11
|
9 | match klasse {
| ^^^^^^ pattern `Dieb` not covered
Warum passiert das?
Der Compiler sagt dir klipp und klar: Du hast die Variante Dieb nicht abgedeckt (pattern 'Dieb' not covered). Rust geht auf Nummer sicher. Es könnte ja sein, dass jemand die Funktion aufruft und einen Dieb übergibt. Da es keinen Code-Pfad für den Dieb gibt, wüsste der Computer nicht, was er tun soll.
Die Lösung:
Du musst die fehlende Variante hinzufügen:
#![allow(unused)]
fn main() {
fn spiele_sound_ab_korrigiert(klasse: CharakterKlasseSimple) {
match klasse {
CharakterKlasseSimple::Krieger => println!("Klirr! Schwert gezogen."),
CharakterKlasseSimple::Magier => println!("Zisch! Feuerball bereit."),
CharakterKlasseSimple::Dieb => println!("Taps, taps... Leise Schritte."), // Gelöst!
}
}
}
Tipp für Faule (oder für riesige Enums): Wenn dir manche Varianten egal sind, kannst du das Unterstrich-Symbol (
_) als „Muster für alles andere“ benutzen. Das ist die sogenannte Wildcard:#![allow(unused)] fn main() { match klasse { CharakterKlasseSimple::Magier => println!("Zisch! Feuerball bereit."), _ => println!("Ein normaler Kampfgeräusch-Sound."), // Trifft auf Krieger und Dieb zu } }
Fehler 2: Falscher Datenzugriff ohne Pattern Matching
Wenn du von Sprachen wie Python, TypeScript oder Java kommst, bist du es gewohnt, direkt auf die Eigenschaften eines Objekts zuzugreifen. Du versuchst vielleicht Folgendes:
#![allow(unused)]
fn main() {
// Wir erstellen einen Magier
let mein_held = CharakterKlasse::Magier { zauberstab_kraft: 100 };
// FEHLER: Wir versuchen direkt auf den Wert zuzugreifen!
// println!("Kraft: {}", mein_held.zauberstab_kraft);
}
Wenn du die Zeile mit dem Kommentar einkommentierst, schreit der Compiler sofort auf:
error[E0609]: no field `zauberstab_kraft` on type `CharakterKlasse`
--> src/main.rs:5:34
|
5 | println!("Kraft: {}", mein_held.zauberstab_kraft);
| ^^^^^^^^^^^^^^^^
Warum darf ich nicht direkt darauf zugreifen?
Überlege mal: Zur Laufzeit des Programms weiß der Computer erst einmal nur, dass in der Variable mein_held irgendeine CharakterKlasse steckt. Es könnte in diesem Moment auch ein Dieb sein! Und ein Dieb hat nun mal kein Feld namens zauberstab_kraft. Würdest du direkt darauf zugreifen dürfen, würde das Programm abstürzen. Rust schützt dich vor diesem Absturz.
Die Lösung:
Du musst den Wert immer über Pattern Matching (z. B. mit match oder if let) auspacken. Nur so stellt Rust sicher, dass der Wert auch wirklich existiert:
#![allow(unused)]
fn main() {
// Die sichere Lösung mit "if let" (die kurze Schwester von match)
// Wir prüfen: Ist es ein Magier? Wenn ja, gib uns die zauberstab_kraft!
if let CharakterKlasse::Magier { zauberstab_kraft } = mein_held {
println!("Erfolg! Die Zauberkraft beträgt: {}", zauberstab_kraft);
} else {
println!("Dieser Charakter ist kein Magier, also hat er auch keine Zauberkraft!");
}
}
8. Ein vollständiges, kompilierbares Programm mit Tests
Damit du alles direkt ausprobieren kannst, findest du hier ein vollständiges Programm. Du kannst es kopieren, in dein Projekt einfügen und mit dem Befehl cargo test ausführen, um zu sehen, wie die Tests grün werden!
// ----------------------------------------------------
// 1. Definition unseres Enums
// ----------------------------------------------------
#[derive(Debug, PartialEq)] // Diese Zeile erlaubt es uns, das Enum zu vergleichen und auszugeben
pub enum CharakterKlasse {
Krieger { schwert_gewicht_kg: f64 },
Magier { zauberstab_kraft: u32 },
Dieb,
}
// ----------------------------------------------------
// 2. Funktion, die das Enum nutzt
// ----------------------------------------------------
pub fn ermittle_kampfschaden(klasse: &CharakterKlasse) -> u32 {
match klasse {
// Ein Krieger macht Schaden basierend auf dem Gewicht seines Schwerts
CharakterKlasse::Krieger { schwert_gewicht_kg } => {
if *schwert_gewicht_kg > 10.0 {
150 // Extrem schweres Schwert!
} else {
75 // Normales Schwert
}
}
// Ein Magier macht Schaden basierend auf seiner Zauberstab-Stärke
CharakterKlasse::Magier { zauberstab_kraft } => {
zauberstab_kraft * 2
}
// Ein Dieb macht immer einen festen, hinterhältigen Schaden
CharakterKlasse::Dieb => 50,
}
}
// ----------------------------------------------------
// 3. Unser Hauptprogramm
// ----------------------------------------------------
fn main() {
let magier = CharakterKlasse::Magier { zauberstab_kraft: 80 };
let schaden = ermittle_kampfschaden(&magier);
println!("Der Magier verursacht {} Schaden!", schaden);
}
// ----------------------------------------------------
// 4. Automatische Tests zum Ausprobieren
// ----------------------------------------------------
#[cfg(test)]
mod tests {
use super::*; // Importiert alles von oben in unser Test-Modul
#[test]
fn test_krieger_schaden() {
let leichter_krieger = CharakterKlasse::Krieger { schwert_gewicht_kg: 5.0 };
let schwerer_krieger = CharakterKlasse::Krieger { schwert_gewicht_kg: 12.5 };
assert_eq!(ermittle_kampfschaden(&leichter_krieger), 75);
assert_eq!(ermittle_kampfschaden(&schwerer_krieger), 150);
}
#[test]
fn test_magier_schaden() {
let magier = CharakterKlasse::Magier { zauberstab_kraft: 50 };
assert_eq!(ermittle_kampfschaden(&magier), 100);
}
#[test]
fn test_dieb_schaden() {
let dieb = CharakterKlasse::Dieb;
assert_eq!(ermittle_kampfschaden(&dieb), 50);
}
}
9. Zusammenfassung
Du hast es geschafft! Du hast eines der wichtigsten und mächtigsten Konzepte von Rust gelernt. Lass uns noch einmal kurz zusammenfassen, was du dir merken solltest:
- Enums sind Aufzählungen von verschiedenen Möglichkeiten (Varianten). Eine Variable kann immer nur genau eine Variante annehmen.
- In Rust können Enums eigene Daten transportieren (z. B. Zahlen, Kommazahlen oder sogar andere Strukturen).
- Mit
matchsortieren wir die Varianten und holen die Daten im Inneren sicher ans Licht. - Der Compiler verlangt Vollständigkeit bei
match. Vergessen ist unmöglich! - Du kannst nicht direkt auf Daten einer Variante zugreifen, ohne vorher mit
matchoderif letsicherzustellen, dass die Variante wirklich vorliegt. Das verhindert Abstürze zur Laufzeit.
In den nächsten Kapiteln werden wir sehen, wie Rust dieses Konzept nutzt, um ein anderes großes Programmierproblem komplett zu lösen: den berühmt-berüchtigten Null-Pointer-Fehler! Aber für heute darfst du stolz sein, die Grundlagen der Enumerationen gemeistert zu haben. Auf ins nächste Abenteuer!