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
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) falseimpl Typ { ... }fügt Methoden hinzu- Das erste Argument ist
self:&self(lesen),&mut self(ändern),self(verbrauchen) - Funktionen ohne
selfsind “assoziierte Funktionen”, aufgerufen mitPerson::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
EndeMit 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.
| Methode | Wirkung |
|---|---|
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
7Merke
structbündelt Felder;implgibt Methoden (&self,&mut self,self) undTyp::neu()-Funktionen#[derive(Debug, Clone, PartialEq)]erzeugt Standardverhaltenenum-Varianten tragen Daten;matcherzwingt Vollständigkeit- Es gibt kein
null:Option<T>mitSome/None - Implementiere
Displaymitwrite!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
Was ist das Besondere an Rust-Enums?
- Varianten können Daten tragen (richtig)
- Sie sind nur Zahlen
- Sie können nicht in match verwendet werden
- Sie sind immer Copy
Wie drückt Rust "kein Wert" aus?
- Mit Option::None (richtig)
- Mit null
- Mit undefined
- Mit 0
Was bedeutet &mut self in einer Methode?
- Die Methode leiht sich das Objekt veränderlich aus (richtig)
- Die Methode verbraucht das Objekt
- Die Methode kopiert das Objekt
- Die Methode ist statisch
Weiter im Kurs
Zurück: Ownership, Borrowing und Lebensdauern
Weiter: Fehlerbehandlung mit Result
Alle Kapitel: Rust im Überblick