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 enum
s 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.