Primitive Typen
Rust ist eine stark typisierte Sprache. Alle Typen aller Werte stehen zur Kompilierzeit fest. Primitive Typen sind fester Bestandteil der Sprache Rust. Über die Standardbibliothek stellt Rust weitere nicht-primitive Typen zur Verfügung. Einige davon werden wir in späteren Kapiteln kennen lernen.
Primitive Skalare
Rust verfügt über vier Arten primitiver skalarer Typen, die in vielen anderen Programmiersprachen ebenfalls Verwendung finden. Dabei handelt es sich um
- Wahrheitswerte (engl. booleans), die nur
true
oderfalse
sein können, und - Zeichen (engl. characters).
- ganze Zahlen (engl. integers),
- Gleitkommazahlen (engl. floating point numbers),
Wahrheitswerte können mit dem Schlüsselwort bool
annotiert werden. Für Zeichen gibt es char
.
Der Schnipsel
#![allow(unused)] fn main() { let b: bool = true; }
legt fest, dass die Variable b vom Typ bool
ist. Oft ist die Typannotation (: bool
) nicht notwendig und
der Compiler kann den Typ der Variable herleiten.
#![allow(unused)] fn main() { let b = true; }
Nichtsdestotrotz stehen alle Typen zur Kompilierzeit fest, ob hergeleitet oder annotiert. Rust ist eine stark typisierte Sprache.
Bei numerischen Datentypen wird üblicherweise der annehmbare Größenbereich im Namen des Typs kodiert. Beispielsweise ist u8
eine ganze Zahl ohne Vorzeichen, die 8 Bit zur Verfügung hat und
dementsprechend mindestens den Wert 0 und höchstens den Wert 255 annhemen kann. Einen Überblick über ganzzahlige
Typen verschafft die folgende Tabelle.
Typ | Min | Max |
---|---|---|
u8 | 0 | 255 |
u16 | 0 | 65 535 |
u32 | 0 | 4 294 967 295 |
u64 | 0 | 264-1 |
u128 | 0 | 2128-1 |
i8 | -128 | 127 |
i16 | -32 768 | 32 767 |
i32 | -2 147 483 648 | 2 147 483 647 |
i64 | -(263) | 263-1 |
i128 | -(2127) | 2127-1 |
Des Weiteren gibt es die plattformabhängigen ganzzahligen Typen usize
und isize
. Diese haben auf einem System
die gleiche Bitgröße. Der obigen Namensgebung folgend ist
isize
vorzeichenbehaftet, während usize
nur positive Werte und 0 annehmen kann. Speicheradressen können höchstens
so groß sein wie der maximale Wert den isize
annehmen kann. So ist sichergestellt, dass Differenzen
von Speicheradressen immer als isize
darstellbar sind.
Darüber hinaus gibt es die Gleitkommazahlen f32
und f64
, die den Formaten entsprechen, die im Standard IEEE 754-2008
als single und double bezeichnet werden. Gleitkommavariablen in Rust können immer positive und negative Werte annehmen.
Overflows
Wenn beispielsweise einem u8
der Wert 256 zugewiesen wird, entsteht ein sogenannter Overflow, da 256 nicht
als u8
dargestellt werden kann.
#![allow(unused)] fn main() { let a: u8 = 256; }
In Rust können verschiedene Fälle eintreten.
- Der Compiler entdeckt den Overflow und verweigert die Übersetzung, was aber auch ausgeschaltet werden kann.
- Bei einem Debug-Build, der zusätzliche Überprüfungen beinhaltet, wird das Programm mit einem Laufzeitfehler abgebrochen.
- Bei einem Release-Build, der optimiert ist und mit so wenig Überprüfungen wie möglich auskommen will,
läuft das Programm weiter und der Overflow findet stillschweigend statt. Das heißt
a
im oberen Beispiel hat den Wert0
.
Sich auf Overflows zu verlassen und bewusst damit zu rechnen ist keine gute Idee und zu vermeiden, da es zu schwer nachvollziehbaren Programmabläufen führen kann.
Casting primitiver Skalare
Skalare eines numerischen Typs können in einen anderen numerischen Typen umgewandelt werden.
Das funktioniert in Rust nur explizit
mit dem Schlüsselwort as
. Selbst, wenn ich eine u8
-Variable, die als u128
darstellbar ist, einer u128
-Varible
zuweisen will, muss ich casten.
#![allow(unused)] fn main() { let a: u8 = 0; let b: u128 = a as u128; }
Auch umgekehrtes casten ist möglich. Dabei besteht das Risiko eines Overflows.
#![allow(unused)] fn main() { let a: u128 = 256; let b: u8 = a as u8; assert_eq!(b, 0); }
Primitive zusammengesetzte Typen
Der Zweck zusammengesetzter Typen (engl. compound types) ist es, mehrere Werte in einem zu gruppieren. Tupel und Arrays sind die beiden primitiven zusammengesetzten Typen, die Rust mitbringt. Während ein Array mehrere Werte gleichen Typs gruppiert, kann ein Tupel mehrere Werte unterschiedlichen Typs beherbergen. Die Anzahl der Werte muss in beiden Fällen zur Kompilierzeit festgelegt werden.
Arrays
Um Arrays anzulegen verwendet man eckige Klammern.
#![allow(unused)] fn main() { let arr = [5, 3, 2, 5]; }
Die gleichwertige Befüllung kann folgendermaßen abgekürzt werden.
#![allow(unused)] fn main() { let arr = [0; 27]; }
einen Array mit 27 Nullen. Man beachte, dass die 27 zur Kompilierzeit fixiert sein muss. Auf Arrayelemente greift man per Index beginnend mit 0 zu.
#![allow(unused)] fn main() { let arr = [0; 27]; let b = arr[5]; }
Wenn man einen ungültigen Index verwendet, beendet Rust kontrolliert das Programm, anstatt einen Zugriff auf den Speicherbereich zuzulassen, der nicht mehr dem Array zugeordnet ist.
Tupel
Ein Tupel bestehend aus einer ganzen Zahl und einem Wahrheitswert kann folgerndermaßen angelegt werden.
#![allow(unused)] fn main() { let t = (5, true); }
Um auf einzelne Elemente eines Tupels zuzugreifen, gibt es den .
-Operator.
Im obigen Beispiel kann man auf den Wert 5 per t.0
und auf den Wert
true
per t.1
zugreifen. Im Unterschied zu Indizes von Arrays müssen
die Indizes von Tupeln zur Kompilierzeit festgelegt werden.
Auf Tupelelemente per .
-Operator und Positionsindex zuzugreifen, kann unübersichtlich werden.
Oft ist es der Lesbarkeit zuträglich, Tupel in benamte Variablen zu entpacken.
#![allow(unused)] fn main() { let t = (5, true); let (num_of_showers, is_still_dirty) = t; }
Das leere Tupel ()
ist ein zulässiger Typ in Rust und hat eine spezielle Bedeutung, die wir im
Abschnitt über Funktionen kennen lernen werden.
Beispiele für Typ-Annotationen von Variablen
Um den Wert einer Variablen festzulgen, kann man diese annotieren. Beispielsweise erstellt man einen Wahrheitswert mit
#![allow(unused)] fn main() { let x: bool = true; }
ein Zeichen mit
#![allow(unused)] fn main() { let c: char = '🥰'; }
eine Gleitkommazahl mit
#![allow(unused)] fn main() { let f: f64 = 1.5; }
eine ganze Zahl mit
#![allow(unused)] fn main() { let a: i32 = 5; }
ein dreielementiges Array von Gleitkommazahlen mit
#![allow(unused)] fn main() { let numbers: [f32; 3] = [0.0, 0.1, 0.7]; }
und ein Tupel bestehend aus einem Wahrheitswert und einem Zeichen mit
#![allow(unused)] fn main() { let t: (bool, char) = (false, 'x'); }
In vielen Fällen ist eine Annotierung des Typen nicht nötig. Oft kann der Compiler den Typ eines Wertes aus dem Kontext herleiten. Keine der obigen Annotationen ist für eine erfolgreiche Übersetzung des Programmquelltextes notwendig. Später werden uns Beispiele begegnen, bei denen wir dem Compiler werden durch Annotationen helfen müssen.