Aufzählungs- und Strukturtypen

Aufzählungstypen

Unter Aufzählungstypen verstehen wir Typen, deren Wert aus einer Aufzählung unterschiedlicher Varianten angenommen werden muss. Das entscheidende Schlüsselwort lautet enum. Beispielsweise repräsentiert der folgende Aufzählungstyp eine der RGB-Farben.

#![allow(unused)]
fn main() {
enum Color {
    Red,
    Green,
    Blue
}
}

Um entsprechend des Wertes einer Variable vom Typ Color handeln zu können, lernen wir eine weitere Möglichkeit kennen, den Kontrollfluss zu beeinflussen. Das Schlüsselwort match wird dafür im folgenden Schnipsel verwendet.

#![allow(unused)]
fn main() {
enum Color {
    Red,
    Green,
    Blue
}
let clr = Color::Red;
let clr_code = match clr {
    Color::Red => [255, 0, 0],
    Color::Green => [0, 255, 0],
    Color::Blue => [0, 0, 255],
};
println!("{}, {}, {}", clr_code[0], clr_code[1], clr_code[2]);
}

Auch bei match handelt es sich um einen Ausdruck. Die entsprechende Verzweigung gibt einen Wert zurück. Man beachte, dass man bei einem match alle möglichen Wertemöglichkeiten eines enum beachten muss. Dementsprechend kompiliert

#![allow(unused)]
fn main() {
// kompiliert nicht!
enum Color {
    Red,
    Green,
    Blue
}
let clr = Color::Red;
let clr_code = match clr {
    Color::Red => [255, 0, 0],
    Color::Green => [0, 255, 0],
};
println!("{}, {}, {}", clr_code[0], clr_code[1], clr_code[2]);
}

nicht, da Color::Blue fehlt. Man kann mit _ ein Sammelbecken für alle übrigen möglichen Werte erzeugen.

#![allow(unused)]
fn main() {
enum Color {
    Red,
    Green,
    Blue
}
let clr = Color::Red;
let clr_code = match clr {
    Color::Red => [255, 0, 0],
    _ => [0, 0, 0],
};
println!("{}, {}, {}", clr_code[0], clr_code[1], clr_code[2]);
}

Die einzelnen Varianten des enums können auch Werte unterschiedlichen Typs beherbergen. Wir können somit Werte unterschiedlichen Typs in einem Aufzählungstyp packen und dann mehrere davon in einem Array beherbergen.

#![allow(unused)]
fn main() {
enum Value {
    Bool(bool),
    Float(f64),
    Int(i32)
}
let arr = [Value::Bool(true), Value::Float(1.4), Value::Int(4)];
}

Der match-Ausdruck hilft auch beim auspacken der Werte aus dem enum. Dieses Auspacken wird Pattern Matching genannt.

#![allow(unused)]
fn main() {
enum Value {
    Bool(bool),
    Float(f64),
    Int(i32)
}
let arr = [Value::Bool(true), Value::Float(1.4), Value::Int(4)];
for a in arr {
    match a {
        Value::Bool(b) => println!("{b}"),
        Value::Float(x) => println!("{x}"),
        Value::Int(i) => println!("{i}"),
    }
}
}

Wenn man sich nur für eine Variante interessiert, kann man auch if-let für das Pattern Matching verwenden.

#![allow(unused)]
fn main() {
enum Value {
    Bool(bool),
    Float(f64),
    Int(i32)
}
let arr = [Value::Bool(true), Value::Float(1.4), Value::Int(4)];
for a in arr {
    if let Value::Float(x) = a {
        println!("{x}");
    }
}
}

Eine while-let-Schleife existiert ebenfalls.

#![allow(unused)]
fn main() {
enum Value {
    Bool(bool),
    Float(f64),
    Int(i32)
}
let arr = [Value::Bool(true), Value::Float(1.4), Value::Int(4)];
while let Value::Float(x) = a {
    println!("{x}");
}
}

Im Unterschied zum vorherigen Beispiel bricht die while-let-Schleife ab, sobald das Pattern Matching fehl schlägt.

Strukturtypen

Mit einem struct kann man mehrere Werte in einem Typen gruppieren.

Beispielsweise repräsentiert

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}
}

einen 3d-Punkt. Wir erzeugen nun eine Variable vom Typ Point.

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}
let v = Point {
    x: 42.0,
    y: 73.0,
};
}

Man nennt v eine Instanz von Point. Rust stellt des Weiteren Tupel-Strukturtypen bereit. Diese gleichen im Wesentlichen den Tupeln, die wir bereits kennen. Sie haben nur einen Namen.

#![allow(unused)]
fn main() {
struct Color(u8, u8, u8);
let clr = Color(255, 211, 123);
println!("{}, {}, {}", clr.0, clr.1, clr.2);
}

Methoden

Nehmen wir an, wir wollen die Länge eines Vektors berechnen. Dabei drängt sich die Organisation des Codes mit einer Funktion auf.

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}
fn squared_dist_to_0(v: Point) -> f64 {
    v.x * v.x + v.y * v.y
}
}

Alternativ kann man auch eine Methode der Struktur implementieren. Dazu müssen wir einen separaten Implementierungsblock verwenden.

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}
impl Point {
    fn squared_dist_to_0(self) -> f64 {
        self.x * self.x + self.y * self.y
    }
}
}

Nun kann man mit

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}
impl Point {
    fn squared_dist_to_0(self) -> f64 {
        self.x * self.x + self.y * self.y
    }
}
let p = Point{ x: 0.1, y: 0.2 };
let d = p.squared_dist_to_0();
println!("{d}");
}

den Abstand zum Ursprung bestimmen. Die Methode squared_dist_to_0 wird auf der Instanz p aufgerufen. Strukturtypen können auch Methoden zugeordnet werden, die keinen Parameter self haben. Das heißt, sie benötigen keine Instanz des Typen. Syntaktisch verwendet man einen doppelten Doppelpunkt :: anstatt eines einfachen Punktes ., um eine direkt dem Typen zugeordnete Methode aufzurufen anstatt eine Methode auf einer Instanz. Im folgenden Beispiel wird die Methode unit verwendet, um einen Einheitsvektor zu instanziieren.

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}
enum Axis { X, Y }
impl Point {
    fn unit(axis: Axis) -> Self {
        match axis {
            Axis::X => Point { x: 1.0, y: 0.0 },
            Axis::Y => Point { x: 0.0, y: 1.0 },
        }
    }
    fn squared_dist_to_0(self) -> f64 {
        self.x * self.x + self.y * self.y
    }
}
let e1 = Point::unit(Axis::Y);
assert!((e1.x).abs() < 1e-12);
assert!((e1.y - 1.0).abs() < 1e-12);
}

Der Rückgabewert der Methode unit ist Self. Das ist eine Alias für den Strukturtyp, dem die Methode zugeordnet ist. In diesem Fall könnte man also auch Point schreiben.

Auch Aufzählungstypen können mit Methoden erweitert werden.

#![allow(unused)]
fn main() {
enum Color {
    Red,
    Green,
    Blue
}
impl Color {
    fn color_code(self) -> [u8; 3] {
        match self {
            Color::Red => [255, 0, 0],
            Color::Green => [0, 255, 0],
            Color::Blue => [0, 0, 255],
        }
    }
}
let clr = Color::Red;
let clr_code = clr.color_code();
println!("{}, {}, {}", clr_code[0], clr_code[1], clr_code[2]);
}

Primitive Typen haben ebenfalls Methoden. Beispielsweise bestimmt man durch die Methode abs den Betrag einer Zahl (engl. absolute value).

#![allow(unused)]
fn main() {
let x: f32 = -1.0;
println!("{}", x.abs());
}

Empfehlenswerterweise ist zu beachten, dass sowohl Methoden als auch Funktionen nur auf die Variablen Zugriff bekommen, die sie auch benötigen. Das lässt sich nicht immer erreichen, darf aber gerne als Ziel dienen. Gerade Methoden machen es einfach, dieses Ziel zu übersehen, da sich hinter self sehr viele Variablen verbergen können. Dabei ist es hilfreich Funktionen und Strukturtypen überschaubar zu halten.

Wir kehren zurück zu unserem Vektor und seiner Länge zurück. Überraschenderweise führt ein zweiter Aufruf der Längenfunktion zu einem Kompilierfehler.

#![allow(unused)]
fn main() {
struct Point {
    x: f64,
    y: f64,
}
impl Point {
    fn squared_dist_to_0(self) -> f64 {
        self.x * self.x + self.y * self.y 
    }
}
let v = Point{x: 0.1, y: 0.2};
let l = v.squared_dist_to_0();
let l_again = v.squared_dist_to_0();
}

Warum das passiert und wie es sich leicht verhindern lässt, erfahren wir im nächsten Kapitel über Ownership.