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

  1. Wahrheitswerte (engl. booleans), die nur true oder false sein können, und
  2. Zeichen (engl. characters).
  3. ganze Zahlen (engl. integers),
  4. 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.

TypMinMax
u80255
u16065 535
u3204 294 967 295
u640264-1
u12802128-1
i8-128127
i16-32 76832 767
i32-2 147 483 6482 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.

  1. Der Compiler entdeckt den Overflow und verweigert die Übersetzung, was aber auch ausgeschaltet werden kann.
  2. Bei einem Debug-Build, der zusätzliche Überprüfungen beinhaltet, wird das Programm mit einem Laufzeitfehler abgebrochen.
  3. 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 Wert 0.

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.