Zum Inhalt springen

Enums in Rust

Ein Enum (kurz für „Enumeration“, also Aufzählung) definiert einen Typ, der genau einen Wert aus einer festen Auswahl annehmen kann. Ein Struct sagt: „Dieses Ding hat alle diese Felder.“ Ein Enum sagt dagegen: „Dieses Ding ist genau eine dieser Möglichkeiten.“

enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("Richtung Norden"),
Direction::South => println!("Richtung Süden"),
Direction::East => println!("Richtung Osten"),
Direction::West => println!("Richtung Westen"),
}
}

Ausgabe:

Richtung Norden
▶ Selbst ausprobieren

Dieses match braucht keinen Auffangzweig mit _. Weil jede mögliche Direction aufgeführt ist, weiß Rust bereits, dass das match vollständig ist.

Hier gehen Enums in Rust weiter als in vielen anderen Sprachen: Jede Variante kann ihre eigenen Daten mitbringen.

enum Shape {
Circle(f64), // Radius
Rectangle(f64, f64), // Breite, Höhe
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14159 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(3.0);
let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle));
println!("{:.2}", area(&rectangle));
}

Ausgabe:

28.27
20.00
▶ Selbst ausprobieren

Shape::Circle enthält einen f64 (den Radius), Shape::Rectangle enthält zwei. match holt diese Daten in jedem Zweig direkt für dich heraus. Du brauchst keinen extra Schritt, um sie „auszupacken“.

Genau wie ein Struct kann ein Enum einen impl-Block mit Methoden haben. In einer Methode schreibst du meistens match self, um für jede Variante zu entscheiden, was passieren soll:

#[derive(Debug)]
enum Light {
Red,
Yellow,
Green,
}
impl Light {
fn seconds(&self) -> u32 {
match self {
Light::Red => 30,
Light::Yellow => 3,
Light::Green => 25,
}
}
fn next(&self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{:?} für {} Sekunden", light, light.seconds());
light = light.next();
}
}

Ausgabe:

Red für 30 Sekunden
Green für 25 Sekunden
Yellow für 3 Sekunden
Red für 30 Sekunden
▶ Selbst ausprobieren

next gibt ein ganz neues Light zurück, und light = light.next(); tauscht das alte dagegen aus. Deshalb ist light als mut deklariert. #[derive(Debug)] funktioniert bei Enums genauso wie bei Structs, also gibt {:?} den Namen der Variante aus.

Steckt die Logik in Methoden, bleibt sie direkt beim Typ. Wer ein Light benutzt, kann light.seconds() aufrufen, ohne zu wissen, wie die Zeiten festgelegt werden. Und fügst du später eine neue Variante hinzu, etwa FlashingYellow, zeigt dir der Compiler jedes match, das du anpassen musst.

Der Typ Option, den du auf seiner eigenen Seite genauer kennenlernst, ist einfach ein Enum aus der Standardbibliothek:

enum Option<T> {
Some(T),
None,
}

Sobald du Enums verstanden hast, wirkt Option nicht mehr wie eine besondere Syntax. Es ist ein ganz normales Enum mit zwei Varianten, von denen eine zufällig einen Wert enthält.

Warum nicht einfach ein Struct mit einem „Typ“-Feld?

Abschnitt betitelt „Warum nicht einfach ein Struct mit einem „Typ“-Feld?“

Du könntest so etwas auch mit einem Struct und einem String-Feld nachbauen, etwa "circle" oder "rectangle". Aber dann hält dich nichts davon ab, aus Versehen "circl" zu tippen. Außerdem müsstest du selbst prüfen, welche Felder für welchen Typ überhaupt gelten. Mit einem Enum kannst du ungültige Kombinationen gar nicht erst hinschreiben, und der Compiler zwingt dich, jede Variante zu behandeln.

Schreib ein Enum Coin mit den Varianten Penny, Nickel, Dime und Quarter (amerikanische Münzen zu 1, 5, 10 und 25 Cent). Schreib fn value_in_cents(coin: &Coin) -> u32 und rechne dann den Inhalt eines Geldbeutels zusammen: ein Quarter, ein Dime, ein Nickel, ein Penny und noch ein Quarter. Tipp: Leg die Münzen in ein Array und lauf mit einer Schleife über &purse.

Lösung anzeigen
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0;
for coin in &purse {
total += value_in_cents(coin);
}
println!("Summe: {total} Cent");
}

Ausgabe:

Summe: 66 Cent
▶ Selbst ausprobieren

Nimm das Enum Shape von dieser Seite und füge eine Variante Triangle hinzu, die die Längen der drei Seiten enthält. Schreib dann eine Funktion perimeter (der Umfang, also die Strecke einmal ganz herum), die für jede Form funktioniert. Tipp: Der Umfang eines Kreises ist 2.0 * 3.14159 * radius. Beim Dreieck addierst du einfach die drei Seiten.

Lösung anzeigen
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn perimeter(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 2.0 * 3.14159 * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:.2}", perimeter(shape));
}
}

Ausgabe:

6.28
18.00
12.00
▶ Selbst ausprobieren

Lösch zum Ausprobieren mal den Zweig Shape::Triangle aus dem match. Rust weigert sich dann zu kompilieren, weil das match nicht mehr jede Variante abdeckt. So stellt der Compiler sicher, dass du nie einen Fall vergisst.

Schreib ein Enum Temperature mit zwei Varianten, die jeweils einen f64 enthalten: Celsius und Fahrenheit. Füge eine Methode to_celsius(&self) -> f64 hinzu und gib Celsius(25.0), Fahrenheit(212.0) und Fahrenheit(50.0) in °C mit einer Nachkommastelle aus. Tipp: Zum Umrechnen nimmst du (f - 32.0) * 5.0 / 9.0.

Lösung anzeigen
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
impl Temperature {
fn to_celsius(&self) -> f64 {
match self {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
fn main() {
let readings = [
Temperature::Celsius(25.0),
Temperature::Fahrenheit(212.0),
Temperature::Fahrenheit(50.0),
];
for t in &readings {
println!("{:.1} °C", t.to_celsius());
}
}

Ausgabe:

25.0 °C
100.0 °C
10.0 °C
▶ Selbst ausprobieren

Weil self ausgeliehen ist, sind c und f Referenzen (&f64). Mit Referenzen kannst du direkt rechnen. Um aber den reinen Celsius-Wert zurückzugeben, schreibst du *c – das heißt „die Zahl, auf die c zeigt“.

  • Ein Enum definiert einen Typ, der eine von mehreren benannten Varianten ist: enum Direction { North, South, ... }.
  • Varianten können eigene Daten enthalten, und verschiedene Varianten können verschiedene Daten enthalten.
  • match zerlegt die Daten eines Enums ganz natürlich und verlangt, dass jede Variante behandelt wird.
  • Enums können in einem impl-Block Methoden haben, genau wie Structs.
  • Option (und Result, das später kommt) sind ganz normale Enums aus der Standardbibliothek – keine Magie.

Kurzer Check

1. Was kann jede Variante eines Enums in Rust, anders als in vielen anderen Sprachen?

2. Warum braucht das match im Direction-Beispiel keinen Auffangzweig mit _?

3. Wie ist Option<T> in der Standardbibliothek tatsächlich umgesetzt?

Punkte: 0 / 3