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
nulldein 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)
f32undf64(Standard).f64hat eine extrem hohe Genauigkeit (doppelte Genauigkeit), um Rundungsfehler zu minimieren.
Wahrheitswerte (bool)
- Kann genau zwei Zustände annehmen:
true(wahr) oderfalse(falsch).
Schriftzeichen (char)
- Speichert genau ein Zeichen in einfachen Anführungszeichen (z. B.
'A'). - In Rust belegt ein
charimmer 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 eineri32-Zahl addieren. Du musst den Konvertierungs-Befehlasnutzen:#![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.
muterlaubt Änderungen. - Shadowing erstellt eine komplett neue Variable mit gleichem Namen.
- Ein
charbelegt 4 Byte und kann Unicode-Symbole und Emojis speichern. - Es gibt kein
null. NutzeOption<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
constundstaticanalysieren. - 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 alsunsafedeklariert 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:
| Methode | Verhalten | Anwendungsfall |
|---|---|---|
wrapping_add | Springt bei Überlauf zurück (z. B. 255 + 1 = 0 bei u8). | Hashfunktionen, Kryptografie |
saturating_add | Verbleibt beim Grenzwert (z. B. 255 + 1 = 255). | Audio-Lautstärken, UI-Koordinaten |
checked_add | Liefert 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): Jedescharbelegt 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
- Debug-Modus: Der Compiler generiert nach arithmetischen Operationen Abfragen dieser Flags (z. B.
intooder bedingte Sprünge wiejofür Jump on Overflow). Wird das Flag gesetzt, löst das Programm einen Systemabbruch (Panic) aus. - 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
charist 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.