EMZETT.
Login

Kurz: impl Person { fn neu(name: &str, alter: u32) -> Self { // “Konstruktor” (Konvention: new) Person { name: name.to_string(), alter } } fn vorstellen(&self) -> String { // &self.

Teil des Kurses Rust

Kapitel 6 von 13 im Kurs Rust (Abschnitt „Eigene Typen“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Structs

#[derive(Debug, Clone, PartialEq)]       // Debug-Ausgabe, Kopieren, Vergleichen automatisch
struct Person {
    name: String,
    alter: u32,
}
 
impl Person {
    fn neu(name: &str, alter: u32) -> Self {         // "Konstruktor" (Konvention: new)
        Person { name: name.to_string(), alter }
    }
    fn vorstellen(&self) -> String {                  // &self: leiht sich die Person
        format!("{} ({})", self.name, self.alter)
    }
    fn geburtstag(&mut self) {                        // &mut self: ändert die Person
        self.alter += 1;
    }
}
 
fn main() {
    let mut mia = Person::neu("Mia", 17);
    mia.geburtstag();
    println!("{}", mia.vorstellen());
    println!("{mia:?}");
    println!("{mia:#?}");
    let kopie = Person { name: "Tom".into(), ..mia.clone() };     // Struct-Update-Syntax
    println!("{} {}", kopie.vorstellen(), mia == kopie);
}

Ausgabe:

Mia (18)
Person { name: "Mia", alter: 18 }
Person {
    name: "Mia",
    alter: 18,
}
Tom (18) false
  • impl Typ { ... } fügt Methoden hinzu
  • Das erste Argument ist self: &self (lesen), &mut self (ändern), self (verbrauchen)
  • Funktionen ohne self sind “assoziierte Funktionen”, aufgerufen mit Person::neu(...)
  • #[derive(...)] erzeugt Standard-Implementierungen

Weitere Formen: Tupel-Structs (struct Meter(f64);) und Unit-Structs (struct Marker;).

Enums: Varianten mit Daten

Ein Rust-enum ist viel mächtiger als in C: Jede Variante kann eigene Daten tragen:

#[derive(Debug)]
enum Form {
    Kreis(f64),                          // ein Wert
    Rechteck { breite: f64, hoehe: f64 }, // benannte Felder
    Punkt,                                // keine Daten
}
 
impl Form {
    fn flaeche(&self) -> f64 {
        match self {
            Form::Kreis(r) => std::f64::consts::PI * r * r,
            Form::Rechteck { breite, hoehe } => breite * hoehe,
            Form::Punkt => 0.0,
        }
    }
}
 
enum Nachricht {
    Beenden,
    Bewege { x: i32, y: i32 },
    Schreibe(String),
}
 
fn main() {
    let formen = [Form::Kreis(1.0), Form::Rechteck { breite: 3.0, hoehe: 4.0 }, Form::Punkt];
    for f in &formen {
        println!("{f:?}: {:.2}", f.flaeche());
    }
    let nachrichten = vec![Nachricht::Schreibe("Hi".into()), Nachricht::Bewege { x: 1, y: -1 }, Nachricht::Beenden];
    for n in nachrichten {
        match n {
            Nachricht::Beenden => println!("Ende"),
            Nachricht::Bewege { x, y } => println!("bewege um {x}/{y}"),
            Nachricht::Schreibe(text) => println!("Text: {text}"),
        }
    }
}

Ausgabe:

Kreis(1.0): 3.14
Rechteck { breite: 3.0, hoehe: 4.0 }: 12.00
Punkt: 0.00
Text: Hi
bewege um 1/-1
Ende

Mit match prüft der Compiler, dass du alle Varianten behandelst. Fügst du später eine hinzu, zeigt er dir jede Stelle, die nachgezogen werden muss.

Option statt null

Rust hat kein null. Für “vielleicht ein Wert” gibt es Option<T>:

fn main() {
    let namen = vec!["Mia", "Tom"];
    let erster: Option<&&str> = namen.first();
    let nummer: Option<i32> = Some(5);
    let nichts: Option<i32> = None;
 
    match nummer {
        Some(n) => println!("Wert {n}"),
        None => println!("kein Wert"),
    }
 
    // Bequeme Methoden
    println!("{}", nichts.unwrap_or(0));
    println!("{}", nummer.map(|n| n * 2).unwrap_or(-1));
    println!("{:?} {:?}", nummer.filter(|n| *n > 10), nummer.and_then(|n| if n > 0 { Some(n * 10) } else { None }));
    println!("{} {}", nummer.is_some(), nichts.is_none());
    println!("{:?}", erster);
 
    if let Some(n) = nummer { println!("if let: {n}"); }
 
    let verschachtelt = namen.get(5).map(|s| s.len()).unwrap_or_default();
    println!("{verschachtelt}");
 
    let zahlen = ["1", "x", "3"];
    let gueltig: Vec<i32> = zahlen.iter().filter_map(|s| s.parse().ok()).collect();
    println!("{gueltig:?}");
}

Ausgabe:

Wert 5
0
10
None Some(50)
true true
Some("Mia")
if let: 5
0
[1, 3]

Der Compiler zwingt dich, den Fall None zu behandeln. Damit verschwindet die berüchtigte Fehlerklasse “Null-Zeiger-Zugriff”. unwrap() und expect("Grund") liefern den Wert oder lösen eine Panic aus, nutze sie nur in Tests, Beispielen oder wenn der Fall unmöglich ist.

MethodeWirkung
unwrap_or(x)Wert oder Ersatz
unwrap_or_else(|| ...)Wert oder berechneter Ersatz
map(f)Wert umwandeln, None bleibt None
and_then(f)verkettet Funktionen, die Option liefern
ok_or(fehler)Option in Result verwandeln
?im Option-Rückgabe: bei None sofort None zurückgeben

Traits ableiten und Display

use std::fmt;
 
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Punkt {
    x: f64,
    y: f64,
}
 
impl fmt::Display for Punkt {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}|{})", self.x, self.y)
    }
}
 
fn main() {
    let p = Punkt { x: 1.5, y: 2.0 };
    let q = p;                                  // Copy: p bleibt gültig
    println!("{p} {q:?} {}", p == q);
    println!("{}", p.to_string().len());
}

Ausgabe:

(1.5|2) Punkt { x: 1.5, y: 2.0 } true
7

Merke

  • struct bündelt Felder; impl gibt Methoden (&self, &mut self, self) und Typ::neu()-Funktionen
  • #[derive(Debug, Clone, PartialEq)] erzeugt Standardverhalten
  • enum-Varianten tragen Daten; match erzwingt Vollständigkeit
  • Es gibt kein null: Option<T> mit Some/None
  • Implementiere Display mit write! für schöne Ausgaben

Übungsaufgabe

Modelliere eine Zahlungsart als enum mit drei Varianten (Karte, PayPal, Rechnung) und schreibe eine Funktion, die für jede die Gebühr berechnet.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Ownership, Borrowing und Lebensdauern

Weiter: Fehlerbehandlung mit Result

Alle Kapitel: Rust im Überblick