Zum Inhalt springen

Structs in Rust

Ein Struct (Struktur) fasst zusammengehörige Werte zu einem eigenen, benannten Typ zusammen. Ein Tupel ist ein schnelles Bündel ohne Namen – ein Struct dagegen gibt jedem Feld einen Namen. Code, der damit arbeitet, wird dadurch viel leichter lesbar.

struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person {
name: String::from("Alice"),
age: 30,
};
println!("{} ist {}", alice.name, alice.age);
}

Ausgabe:

Alice ist 30
▶ Selbst ausprobieren

Vergleich das mal mit einem Tupel ("Alice", 30): Bei einem Struct erklären sich alice.name und alice.age von selbst. Du musst dir nicht merken, dass Feld 0 der Name und Feld 1 das Alter ist.

Wie Variablen sind auch Struct-Instanzen standardmäßig unveränderbar. Willst du ein Feld nach dem Erstellen ändern, schreib mut davor:

struct Person {
name: String,
age: u32,
}
fn main() {
let mut alice = Person {
name: String::from("Alice"),
age: 30,
};
alice.age += 1;
println!("{} ist jetzt {}", alice.name, alice.age);
}
▶ Selbst ausprobieren

Mit einem impl-Block hängst du Funktionen an ein Struct. Darin steht &self für genau die Instanz, auf der die Methode aufgerufen wurde:

struct Person {
name: String,
age: u32,
}
impl Person {
fn greet(&self) {
println!("Hallo, ich bin {} und {} Jahre alt", self.name, self.age);
}
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
alice.greet();
}

Ausgabe:

Hallo, ich bin Alice und 30 Jahre alt
▶ Selbst ausprobieren

Ein verbreitetes Muster ist eine new-Funktion im impl-Block, die dir eine Instanz baut. So muss niemand beim Aufruf jedes Feld von Hand ausschreiben:

struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person { name: name.to_string(), age }
}
}
fn main() {
let bob = Person::new("Bob", 25);
println!("{} ist {}", bob.name, bob.age);
}
▶ Selbst ausprobieren

Achte auf das alleinstehende age in Person { name: name.to_string(), age }. Hat eine Variable genau denselben Namen wie das Feld, kannst du dir age: age sparen und einfach age schreiben. Rust nennt das Field Init Shorthand (Kurzschreibweise für die Feld-Initialisierung).

Versuchst du println!("{:?}", alice) mit dem Struct von oben, kompiliert das nicht. Rust weiß nicht, wie es einen eigenen Typ zum Debuggen ausgeben soll – außer du sagst es ihm. Schreib #[derive(Debug)] über das Struct, und du bekommst das geschenkt:

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
println!("{:?}", alice);
}

Ausgabe:

Person { name: "Alice", age: 30 }
▶ Selbst ausprobieren

Mit #[derive(...)] lässt Rust häufig gebrauchte Funktionalität für einen Typ automatisch erzeugen. Debug und später auch Clone und andere wirst du ständig auf diese Weise hinzufügen.

Schreib ein Struct Rectangle mit den Feldern width und height (u32). Gib ihm zwei Methoden: area, die die Fläche zurückgibt, und is_square, die true zurückgibt, wenn beide Seiten gleich lang sind. Tipp: Beide Methoden lesen die Felder nur, also nehmen sie &self.

Lösung anzeigen
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let wide = Rectangle { width: 8, height: 3 };
let square = Rectangle { width: 5, height: 5 };
println!("breit: Fläche {}, quadratisch? {}", wide.area(), wide.is_square());
println!("Quadrat: Fläche {}, quadratisch? {}", square.area(), square.is_square());
}

Ausgabe:

breit: Fläche 24, quadratisch? false
Quadrat: Fläche 25, quadratisch? true
▶ Selbst ausprobieren

Schreib ein Struct Account mit einem owner (String) und einem Kontostand balance in Cent (u32). Schreib Account::new("Ana"), das den Kontostand bei 0 beginnen lässt. Zahl dann 50 und 25 Cent ein, indem du das Feld änderst, und gib das Konto auf zwei Arten aus: als normalen Satz und mit {:?}. Tipp: Die Variable muss let mut sein, bevor du account.balance ändern kannst, und {:?} braucht #[derive(Debug)].

Lösung anzeigen
#[derive(Debug)]
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
fn main() {
let mut account = Account::new("Ana");
account.balance += 50;
account.balance += 25;
println!("{} hat {} Cent", account.owner, account.balance);
println!("{:?}", account);
}

Ausgabe:

Ana hat 75 Cent
Account { owner: "Ana", balance: 75 }
▶ Selbst ausprobieren

new setzt balance: 0 selbst, sodass niemand, der die Funktion aufruft, es vergessen kann. Genau deshalb sind Konstruktor-Funktionen so verbreitet.

Leg drei Person-Werte in einen Vektor: Ana (34), Ben (16) und Chen (52). Gib den Namen aller aus, die 18 oder älter sind, und danach, wer am ältesten ist. Tipp: Lauf mit for person in &people durch die Liste und merk dir die bisher älteste Person mit let mut oldest = &people[0];.

Lösung anzeigen
struct Person {
name: String,
age: u32,
}
fn main() {
let people = vec![
Person { name: String::from("Ana"), age: 34 },
Person { name: String::from("Ben"), age: 16 },
Person { name: String::from("Chen"), age: 52 },
];
let mut oldest = &people[0];
for person in &people {
if person.age >= 18 {
println!("{} ist volljährig", person.name);
}
if person.age > oldest.age {
oldest = person;
}
}
println!("Am ältesten: {} ({})", oldest.name, oldest.age);
}

Ausgabe:

Ana ist volljährig
Chen ist volljährig
Am ältesten: Chen (52)
▶ Selbst ausprobieren

oldest ist eine Referenz (&Person) in den Vektor hinein, keine Kopie. Es wird also nichts geklont. Jedes Mal, wenn wir jemand Älteren finden, lassen wir oldest einfach auf diese Person zeigen.

  • Ein Struct bündelt benannte Felder in einem Typ: struct Person { name: String, age: u32 }.
  • Eine Instanz erstellst du mit Person { name: ..., age: ... }. Markiere sie mit mut, wenn du Felder später ändern willst.
  • Verhalten fügst du mit einem impl-Block hinzu. &self steht für die Instanz, auf der die Methode aufgerufen wurde.
  • Mit #[derive(Debug)] kannst du ein Struct per {:?} ausgeben.

Kurzer Check

1. Was ist der Hauptvorteil eines Structs gegenüber einem Tupel?

2. Was ermöglicht dir #[derive(Debug)] an einem Struct?

3. Was bewirkt das alleinstehende age in Person { name: name.to_string(), age }?

Punkte: 0 / 3