Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kapitel 03: Fundamente: Variablen und primitive Datentypen

In diesem Kapitel befassen wir uns mit den absoluten Bausteinen jedes Rust-Programms. Sie werden lernen, wie Rust Werte im Speicher ablegt, wie Variablen deklariert werden, wie das Typsystem funktioniert und wie Sie primitive Typen nutzen, um Berechnungen durchzuführen.

In diesem Kapitel bieten wir Ihnen drei verschiedene Perspektiven auf das Thema an. Wählen Sie die Sicht, die am besten zu Ihrem Hintergrund passt:

  • Für Anfänger: Erklärt Variablen, Unveränderlichkeit, Shadowing, grundlegende Datentypen und das sichere Option-Konzept anhand einfacher Vergleiche.
  • Für Profis: Konzentriert sich auf das Newtype-Pattern, statische Zusicherungen, Typ-Ergonomie über Shadowing und Überlauf-Modellierungen im API-Design.
  • Hardware-Sicht: Analysiert die physikalische Repräsentation von Datentypen (Padding, Alignment, UTF-32 chars), Stack-Rahmen-Layouts und die Behandlung von Register-Flags bei Integer-Überläufen.

Begleitvideo zu Kapitel 3: Variablen & primitive Datentypen


Kapitel 03: Fundamente: Variablen & Typen (Sicht für Anfänger)

In diesem Kapitel lernen wir, wie der Computer Informationen im Gedächtnis behält, warum Rust dabei sehr vorsichtig vorgeht und welche Arten von Schachteln es für deine Daten gibt.

1. Lernziele

Du wirst in diesem Abschnitt:

  • Verstehen, was eine Variable im Arbeitsspeicher (RAM) wirklich ist.
  • Erkennen, warum Rust standardmäßig Sekundenkleber auf alle Variablen schmiert (Unveränderlichkeit).
  • Lernen, wie du mit Shadowing Variablen sicher überschreibst.
  • Die verschiedenen Schachtelgrößen (Datentypen) für Zahlen, Kommazahlen, Wahrheitswerte und Emojis kennenlernen.
  • Erfahren, warum das Fehlen von null dein Programm vor schweren Abstürzen rettet.

2. Variablen: Die Aufkleber im Arbeitsspeicher

Stell dir den Arbeitsspeicher (RAM) deines Computers wie ein gigantisches Postgebäude mit Milliarden durchnummerierter Postfächer vor. Jedes Fach kann eine kleine Information aufnehmen. Ohne Programmiersprache müsstest du dir merken: “Meine Zahl liegt in Fach Nr. 832.194.281.” Das ist unmöglich.

Eine Variable ist einfach ein Aufkleber mit einem lesbaren Namen (z. B. alter), den wir auf das Postfach kleben.

#![allow(unused)]
fn main() {
let alter = 30;
}

Ab jetzt weiß der Computer: Wenn wir alter sagen, meinen wir den Inhalt in diesem Postfach.

Sekundenkleber auf den Variablen (Unveränderlichkeit)

In vielen Programmiersprachen darfst du den Wert einer Variablen jederzeit ändern. Rust ist hier anders und standardmäßig sehr misstrauisch. Wenn du eine Variable erstellst, klebt Rust sie sofort fest: Sie ist unveränderlich (immutable).

fn main() {
    let x = 5;
    x = 6; // FEHLER! Der Compiler verbietet das!
}

Warum macht Rust das? Wenn Daten sich nicht heimlich ändern können, ist dein Programm viel sicherer und leichter zu verstehen – besonders wenn später viele Aufgaben gleichzeitig erledigt werden müssen.

Das Wort mut (Die veränderbare Schüssel)

Wenn du eine Variable brauchst, die sich verändern darf (z. B. einen Punktestand in einem Spiel), musst du das explizit ankündigen. Dafür nutzt du das Wort mut (kurz für mutable, veränderbar):

fn main() {
    let mut punkte = 0;
    punkte = punkte + 10; // Das funktioniert wunderbar!
}

3. Shadowing: Die alte Schachtel wegwerfen

Rust hat einen sehr praktischen Trick namens Shadowing (Variablenüberdeckung). Du darfst denselben Variablennamen mehrmals mit dem Wort let definieren:

fn main() {
    let text = "42";
    let text: i32 = text.parse().unwrap(); // Der Typ ändert sich von Text zu Zahl!
}

Was passiert hier? Im Gegensatz zu mut ändern wir nicht den Inhalt der Schüssel. Wir werfen die alte Schachtel einfach komplett in den Müll und stellen eine brandneue Schachtel mit demselben Namen ins Regal. Das ist genial, wenn du Daten transformierst und keine unschönen Namen wie text_eingabe und zahl_eingabe erfinden willst.


4. Die primitiven Datentypen (Die Schachtelgrößen)

Der Computer muss genau wissen, wie groß das Postfach sein muss. Rust bietet uns folgende Standardformen an:

Ganzzahlen (Integers)

  • Mit Vorzeichen (können negativ sein): i8, i16, i32 (Standard), i64, i128. (Die Zahl gibt die Bit-Größe an).
  • Ohne Vorzeichen (nur positiv oder Null): u8 (geht von 0 bis 255), u16, u32, u64, u128.
  • usize / isize: Die Größe passt sich automatisch an deinen Prozessor an (bei 64-Bit-PCs sind das 8 Byte). Wird meistens für Listen-Indizes verwendet.

Fließkommazahlen (Dezimalzahlen)

  • f32 und f64 (Standard). f64 hat eine extrem hohe Genauigkeit (doppelte Genauigkeit), um Rundungsfehler zu minimieren.

Wahrheitswerte (bool)

  • Kann genau zwei Zustände annehmen: true (wahr) oder false (falsch).

Schriftzeichen (char)

  • Speichert genau ein Zeichen in einfachen Anführungszeichen (z. B. 'A').
  • In Rust belegt ein char immer 4 Byte, weil er jedes Zeichen der Welt (Unicode) inklusive Emojis ('🦀') speichern kann!

Important

Rust konvertiert Typen niemals heimlich! Du kannst eine u8-Zahl nicht mit einer i32-Zahl addieren. Du musst den Konvertierungs-Befehl as nutzen:

#![allow(unused)]
fn main() {
let a: u8 = 10;
let b: i32 = a as i32 + 5;
}

5. Kein null – Der sichere Karton Option<T>

In vielen Sprachen gibt es den Wert null (bedeutet: “Kein Wert vorhanden”). Wenn man darauf zugreift, stürzt das Programm ab. Rust hat kein null. Wenn ein Wert fehlen darf, verpackt Rust ihn in eine Schachtel namens Option:

#![allow(unused)]
fn main() {
// Ein Option-Karton kann zwei Zustände haben:
let ein_wert = Some(42); // Der Karton ist voll und enthält die 42
let kein_wert = None;     // Der Karton ist leer
}

Der Compiler zwingt dich dazu, den Karton erst vorsichtig zu öffnen, bevor du an den Wert herankommst. Dadurch sind Abstürze durch leere Variablen unmöglich!


6. Key Takeaways

  • Variablen sind standardmäßig unveränderlich. mut erlaubt Änderungen.
  • Shadowing erstellt eine komplett neue Variable mit gleichem Namen.
  • Ein char belegt 4 Byte und kann Unicode-Symbole und Emojis speichern.
  • Es gibt kein null. Nutze Option<T> für eventuell fehlende Werte.

Kapitel 03: Fundamente: Variablen & Typen (Sicht für Profis)

Dieses Kapitel behandelt fortgeschrittene Typsystem-Konzepte, das Newtype-Pattern, die Semantik von Konstanten und die Modellierung optionaler Werte.

1. Lernziele

In diesem Abschnitt werden Sie:

  • Das Newtype-Pattern zur Durchsetzung von Typsicherheit auf Domänenebene anwenden.
  • Den Unterschied in der Speicherplatzallokation zwischen const und static analysieren.
  • Shadowing zur Erhöhung der API-Ergonomie und Code-Lesbarkeit nutzen.
  • Strategien zur sicheren Modellierung von numerischen Überläufen im Domänenmodell vergleichen.

2. Item 3: Bevorzuge das Typsystem von Rust gegenüber ad-hoc Annahmen (Newtype-Pattern)

Ein häufiger Fehler in vielen Programmiersprachen ist die Verwendung primitiver Typen für Domänenwerte (z. B. ein f64 für eine Temperatur in Kelvin und ein anderes f64 für Celsius). Dies führt zu logischen Fehlern, die der Compiler nicht erkennen kann.

In Rust nutzen wir das Newtype-Pattern, um Typen auf Domänenebene voneinander abzugrenzen:

// Definition zweier unterschiedlicher Typen ohne Laufzeit-Overhead
struct Celsius(f64);
struct Fahrenheit(f64);

fn main() {
    let temp_c = Celsius(25.0);
    // let temp_f: Fahrenheit = temp_c; // COMPILER-FEHLER: Typkonflikt!
}

Da die Structs als Tupel-Strukturen mit nur einem Feld definiert sind, optimiert der Compiler sie bei der Kompilierung vollständig weg (Zero-Cost Abstraction). Zur Laufzeit existiert nur der primitive f64-Wert.


3. Konstanten (const) vs. Statische Variablen (static)

In Rust müssen Konstanten zur Kompilierzeit auswertbar sein und erfordern zwingend eine explizite Typannotation:

  • const: Besitzt keinen festen Speicherort im RAM. Der Compiler führt bei der Kompilierung ein Inlining durch; der Wert wird direkt als Literal in den Maschinencode kopiert.
  • static: Besitzt eine feste Adresse im Speicher, die über die gesamte Lebensdauer des Programms gültig ist. Statische Variablen erlauben die Deklaration von veränderlichen globalen Zuständen (static mut), deren Zugriff jedoch grundsätzlich als unsafe deklariert werden muss, da Rust keine Garantien für die Threadsicherheit auf dieser Ebene übernehmen kann.

4. Shadowing als ergonomisches API-Design

Professioneller Rust-Code nutzt Shadowing intensiv zur Transformation von Daten. Es verhindert, dass ungültige Zwischenzustände im aktuellen Scope nutzbar bleiben:

#![allow(unused)]
fn main() {
fn verarbeite_eingabe(eingabe_raw: &str) {
    let eingabe = eingabe_raw.trim();
    let eingabe: i32 = eingabe.parse().expect("Ungültiges Format");
    // eingabe ist ab hier sicher als i32 typisiert.
    // Der String-Zustand ist für den Rest der Funktion unzugänglich.
}
}

5. Modellierung von Integer-Überläufen im Domänen-Design

Standardmäßig stürzt Rust im Debug-Modus bei einem Integer-Überlauf (Overflow) mit einer panic ab, während im Release-Modus das mathematische Wrapping (Zweierkomplement-Verhalten) angewendet wird.

Für kritische Berechnungen (z. B. in Finanz- oder Sicherheitsanwendungen) müssen Sie dieses Verhalten explizit steuern:

MethodeVerhaltenAnwendungsfall
wrapping_addSpringt bei Überlauf zurück (z. B. 255 + 1 = 0 bei u8).Hashfunktionen, Kryptografie
saturating_addVerbleibt beim Grenzwert (z. B. 255 + 1 = 255).Audio-Lautstärken, UI-Koordinaten
checked_addLiefert ein Option<T> (None bei Überlauf).Finanzberechnungen, API-Validierung

6. Key Takeaways (Architektur-Richtlinien)

  • Newtypes: Kapseln Sie primitive Datentypen in eigene Structs, um logische Verwechslungen zur Kompilierzeit unmöglich zu machen.
  • Arithmetic Safety: Verlassen Sie sich nicht auf das standardmäßige Überlaufverhalten; nutzen Sie explizite Methoden (checked_add, saturating_add).
  • Ergonomie: Nutzen Sie Shadowing zur Bereinigung von Scopes bei der Typkonversion.

Kapitel 03: Fundamente: Variablen & Typen (Hardware- & Systemsicht)

Dieses Kapitel analysiert die Repräsentation von primitiven Typen im Speicher, das Phänomen des Speicher-Paddings, Alignment-Regeln der CPU und die mathematische Repräsentation von Überläufen im Statusregister.

1. Lernziele

Sie werden in diesem Abschnitt:

  • Die Speichergröße und das Alignment von primitiven Typen auf Systemebene berechnen.
  • Das Konzept des Speicher-Paddings (Füllbytes) zur Gewährleistung effizienter Speicherzugriffe verstehen.
  • Die Repräsentation des char-Typs als UTF-32-Scalar auf Bitebene analysieren.
  • Die Behandlung von Überläufen über die Statusflags des Prozessors nachvollziehen.

2. Speicher-Alignment und Padding auf Hardware-Ebene

Moderne CPUs greifen auf das RAM nicht byte-weise, sondern in Worten (Wörtern) von meist 4 oder 8 Bytes (z. B. 64-Bit-Worte) zu. Um die Leseleistung zu maximieren, müssen Daten im Speicher an Adressen liegen, die ein Vielfaches ihrer eigenen Größe sind (Alignment).

Wenn wir verschiedene Typen mischen, fügt der Compiler unsichtbare Füllbytes (Padding) ein:

Typ-Layout im Speicher:
let a: u8  (1 Byte)  --> [ a ]
let b: u32 (4 Bytes) --> [ P ] [ P ] [ P ] [ b ] [ b ] [ b ] [ b ]
                         (P = Padding-Bytes für 4-Byte-Alignment von b)

Würde Rust das Padding nicht einfügen, müsste die CPU für den Zugriff auf b zwei Speicherzugriffe durchführen und die Bits verschieben, was die Leistung massiv beeinträchtigen würde.


3. Physikalische Repräsentation von char und String

Ein char in Rust ist nicht mit einem char in C/C++ (welches 1 Byte groß ist und der ASCII-Tabelle entspricht) zu vergleichen.

  • char (UTF-32): Jedes char belegt exakt 4 Bytes im Speicher. Dies entspricht einem Unicode-Codepoint. Der Wert wird intern als 32-Bit-Ganzzahl (UTF-32) repräsentiert, was den direkten Zugriff auf Zeichen wie Emojis (‘🦀’) auf CPU-Ebene ohne Dekodierungsaufwand ermöglicht.
  • String / &str (UTF-8): Textketten werden in Rust als UTF-8-kodierte Byte-Arrays gespeichert. Hier belegt ein Zeichen je nach Symbol zwischen 1 und 4 Bytes. Der Buchstabe 'a' belegt im String 1 Byte, das Emoji '🦀' jedoch 4 Bytes.

4. Integer-Überlauf auf CPU-Ebene

Auf Prozessorebene wird jede arithmetische Operation von der ALU (Arithmetic Logic Unit) durchgeführt. Die ALU setzt bei Berechnungen Flags im Statusregister (z. B. FLAGS bei x86):

  • Overflow Flag (OF): Wird gesetzt, wenn das Ergebnis einer vorzeichenbehafteten Operation das Vorzeichenbit verfälscht (Überlauf bei i32).
  • Carry Flag (CF): Wird gesetzt, wenn eine vorzeichenlose Operation einen Übertrag erfordert (Überlauf bei u32).

Wie Rust Flags nutzt

  1. Debug-Modus: Der Compiler generiert nach arithmetischen Operationen Abfragen dieser Flags (z. B. into oder bedingte Sprünge wie jo für Jump on Overflow). Wird das Flag gesetzt, löst das Programm einen Systemabbruch (Panic) aus.
  2. Release-Modus: Der Compiler ignoriert diese Flags bei Standardoperationen. Die CPU verwirft das überlaufende Bit hardwareseitig (klassisches Zweierkomplement-Wrapping), was maximale Geschwindigkeit garantiert.

5. Key Takeaways (Systemebene)

  • Alignment erzwingt, dass Daten an Speicheradressen liegen, die ihrer Größe entsprechen, was zu Padding-Bytes führen kann.
  • Ein char ist ein 4-Byte-UTF-32-Wert; Strings hingegen nutzen die kompaktere UTF-8-Codierung auf Heap-Ebene.
  • Überläufe werden direkt von CPU-Hardwareflags (OF, CF) gemeldet, was Rust im Debug-Modus zur Absicherung abfängt.