Modul 5 — Enums & Pattern Matching
Enums: Varianten mit Daten
Aus Modul 4 kennst du Structs: alle Felder gleichzeitig. Ein Enum ist das Gegenteil: entweder/oder. Ein Wert ist genau eine von mehreren Varianten — und jede Variante kann ihre eigenen Daten tragen:
enum Shape {
Circle { radius: f64 },
Rectangle(f64, f64),
Point,
}
fn area(s: Shape) -> f64 {
match s {
Shape::Circle { radius } => 3.14159 * radius * radius,
Shape::Rectangle(w, h) => w * h,
Shape::Point => 0.0,
}
}
fn main() {
println!("{}", area(Shape::Circle { radius: 2.0 }));
println!("{}", area(Shape::Rectangle(3.0, 4.0)));
}
Drei Varianten, drei verschiedene Formen von Daten: struct-artig (benannte Felder), tuple-artig (Positionen) und ganz ohne Daten. So modellierst du „eins von“ ohne Kompromisse.
Enums verhalten sich wie Typen
Auch Enums bekommen impl-Blöcke mit Methoden — das Verhalten hängt an der Familie, nicht an der einzelnen Variante:
enum Response {
Ok,
NotFound,
ServerError(String),
}
impl Response {
fn is_success(&self) -> bool {
matches!(self, Response::Ok)
}
}
fn main() {
let r = Response::ServerError(String::from("DB down"));
println!("{}", r.is_success());
}
matches! ist ein Makro für die kompakte Ja/Nein-Frage: „Ist das dieser
Variant?“ Für alles Detaillierteres nimm ein echtes match.
Enum vs. Struct — die Modellierungsfrage
- Struct: „das Ding HAT all diese Daten“ (gleichzeitig)
- Enum: „das Ding IST genau eins von diesen“ (alternativ)
HTTP-Antworten sind ein Enum (Ok ODER NotFound ODER ServerError); eine HTTP-Antwort mit Headern und Body wäre ein Struct.
📝 Schnellprüfung
Welche Variantenformen kann ein Rust-Enum haben?
Übung: HTTP-Linien
🧩 Übung m5-l1-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Option & Result — kein null, keine Exceptions
Die zwei wichtigsten Enums der ganzen Sprache sind Standard: Option und Result. Sie ersetzen null und Exceptions — durch Typen.
fn find_price(catalog: &[(&str, u32)], article: &str) -> Option<u32> {
for (name, price) in catalog {
if *name == article {
return Some(*price);
}
}
None
}
fn main() {
let catalog = [("Kaffee", 7), ("Tee", 4)];
match find_price(&catalog, "Tee") {
Some(price) => println!("Preis: {price}"),
None => println!("unbekannt"),
}
}
Option ist Some(wert) oder None. Es gibt keinen dritten Weg —
und der Compiler verlangt, dass du beide Fälle bedachtest, bevor du an den
Wert kommst. Der Milliarden-Dollar-Fehler „null reference“ existiert in Rust
nicht.
Result: Fehler sind Werte
fn parse_age(s: &str) -> Result<u32, String> {
match s.parse::<u32>() {
Ok(age) => Ok(age),
Err(_) => Err(String::from("keine gültige Zahl")),
}
}
fn main() {
println!("{:?}", parse_age("42"));
println!("{:?}", parse_age("abc"));
}
Result<T, E> ist Ok(T) oder Err(E) — für alles, was scheitern kann
und soll: Parsen, Dateien, Netzwerk. Der Aufrufer KANN den Fehler nicht
ignorieren, ohne es explizit zu tun.
Panic ist kein Fehlerhandling
panic! bricht ab — für echte Bugs und gebrochene Invarianten. Alles, was
erwartbar schiefgehen kann (falsche Eingabe, fehlende Datei), gehört in
Result. unwrap() auf einem Err oder None paniziert: gut für Tests und
Prototypen, tabu in Produktion.
📝 Schnellprüfung
Warum ist .unwrap() auf einem Result in Produktionscode gefährlich?
Übung: Suche ohne Panic
🧩 Übung m5-l2-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Match meistern: Muster, Guards, Bindings
match ist mächtiger als switch: Muster destrukturieren, Guards filtern, Bindings fangen Werte. Ein Überblick über die Werkzeuge:
fn classify(temp: i32) -> String {
match temp {
x if x < 0 => String::from("frost"), // Guard
n @ 0..=30 => format!("okay ({n} Grad)"), // Range + @-Binding
_ => String::from("heiß"),
}
}
fn main() {
println!("{}", classify(-5));
println!("{}", classify(21));
println!("{}", classify(35));
}
Destructuring: Muster öffnen Varianten
enum Message {
Move { x: i32, y: i32 },
Say(String),
Ping,
}
fn handle(msg: Message) {
match msg {
Message::Move { x, y } => println!("gehe zu {x},{y}"),
Message::Say(text) if text.len() > 20 => println!("langer Text"),
Message::Say(text) => println!("sage: {text}"),
Message::Ping => println!("pong"),
}
}
fn main() {
handle(Message::Move { x: 3, y: 7 });
handle(Message::Say(String::from("hallo")));
}
Das Muster Move { x, y } nimmt die Felder auseinander — ohne
Zwischenvariablen. Ein Guard (if text.len() > 20) filtert innerhalb eines
Arms; der Compiler prüft weiter auf Erschöpfung.
Erschöpfung ist das Superkonzept
match MUSS jeden Fall abdecken. Fügt dein Team morgen eine Variante hinzu,
bricht jede nicht angepasste Stelle beim Kompilieren — statt zur Laufzeit
stillschweigend falsch zu laufen. _ deaktiviert diese Prüfung bewusst:
Nutze es für „der Rest interessiert nicht“, nie aus Bequemlichkeit.
if let und let else
Nur EIN Fall interessiert? if let ist das kompakte match:
fn main() {
let maybe = Some(42);
if let Some(n) = maybe {
println!("Wert: {n}");
}
let maybe2: Option<i32> = None;
let doubled = if let Some(n) = maybe2 {
n * 2
} else {
0
};
println!("{doubled}");
}
Und seit Rust 1.65 gibt es let ... else für das frühzeitige Ausscheiden aus
der Funktion, wenn der interessante Fall fehlt.
📝 Schnellprüfung
Was macht `n @ 1..=12` im Muster?
Übung: Klassifikator
🧩 Übung m5-l3-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Combinatoren & der ?-Operator
Option und Result haben eine ganze Werkzeugkiste von Methoden, die das match-Boilerplate ersetzen — Combinatoren:
fn main() {
let maybe: Option<i32> = Some(5);
// map: transformiere den Wert, wenn vorhanden
println!("{:?}", maybe.map(|n| n * 2));
// unwrap_or / unwrap_or_else: Fallback statt Panic
println!("{}", maybe.unwrap_or(0));
println!("{}", None::<i32>.unwrap_or_else(|| expensive()));
// ok_or: Option in Result mit Fehlermeldung
let r = maybe.ok_or(String::from("fehlt"));
println!("{r:?}");
}
fn expensive() -> i32 {
42
}
?: der Early-Return für Fehler
Der große Wurf ist der ?-Operator: „Wenn Ok/Some, nimm den Wert. Wenn
nicht, verlasse SOFORT die Funktion mit dem Fehler.“
fn parse_pair(s: &str) -> Result<(i32, i32), String> {
let (a, b) = s
.split_once(',')
.ok_or_else(|| String::from("Komma fehlt"))?;
let x = a.trim().parse::<i32>().map_err(|e| format!("x: {e}"))?;
let y = b.trim().parse::<i32>().map_err(|e| format!("y: {e}"))?;
Ok((x, y))
}
fn main() {
println!("{:?}", parse_pair("12, -3"));
println!("{:?}", parse_pair("7,x"));
}
Drei Fehlerquellen, drei ? — null Boilerplate. Der Fehlerflusse liest sich
wie der Happy-Path, und jede Fehlermeldung ist absichtlich. map_err
übersetzt dabei fremde Fehlertypen in deine.
Die Regel für sauberen Fehlerfluss
- Innere Funktionen geben Result zurück (klein, testbar).
- Fehler mit
?propagieren, mitmap_errverständlich machen. - Nur die äußerste Schicht (main, Handler) entscheidet: Ausgeben, Loggen, Retry — niemals unwrap in der Mitte.
📝 Schnellprüfung
Was passiert bei `expr?`, wenn expr ein Err ist?
Übung: von unwrap zu ?
🧩 Übung m5-l4-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Modul-Checkpoint
Checkpoint: Modul 5
Bestehensgrenze: 80 %. Der Versuch wird bewertet und zählt für das Modul-Gate.
Bereit, dein Wissen zu testen?
Probiere unsere Übungsprüfungen mit Hunderten von realistischen Fragen aus.
Üben starten →