Kapitel 4: Speicherverwaltung leicht gemacht – Ownership und Referenzen für Einsteiger
Willkommen in einem der spannendsten und wichtigsten Kapitel von Rust! Wenn du von Sprachen wie Python, Scratch oder JavaScript kommst, kennst du das vielleicht: Du erstellst Variablen, wirfst Daten hinein und der Computer kümmert sich im Hintergrund darum, wo er das alles abspeichert. In Rust ist das ein bisschen anders – und das aus einem genialen Grund: Rust möchte, dass deine Programme blitzschnell laufen und niemals abstürzen oder Speicherplatz verschwenden.
Dazu nutzt Rust ein System namens Ownership (auf Deutsch: Besitzrecht) und Borrowing (auf Deutsch: Ausleihen). Keine Angst, das klingt komplizierter, als es ist! In diesem Kapitel erklären wir dir diese Konzepte Schritt für Schritt mit einfachen Beispielen und Alltagsbildern, die du sofort verstehst.
1. Was ist Ownership?
Stell dir vor, du hast ein echtes, gedrucktes Lieblingsbuch in deinem Zimmer liegen. Dieses Buch gehört dir. Du bist der Besitzer (Owner) des Buches.
In der Welt von Rust funktioniert das genauso mit Daten (also Zahlen, Wörtern oder Listen). Rust stellt drei goldene Regeln auf, um Ordnung im Speicher des Computers zu halten:
- Jeder Wert (jede Information) hat eine Variable, die sein Besitzer ist. (Genauso wie dein Lieblingsbuch dir gehört.)
- Es kann immer nur einen Besitzer zur gleichen Zeit geben. (Das Buch kann physisch nur in deinem Zimmer liegen oder im Zimmer eines Freundes, aber nicht an beiden Orten gleichzeitig sein.)
- Wenn der Besitzer “weggeht” (seinen Gültigkeitsbereich verlässt), wird der Wert automatisch gelöscht. (Wenn du umziehst und dein Zimmer komplett geräumt wird, wird auch alles darin sauber aufgeräumt.)
Ein Geltungsbereich (Scope) in der Praxis
Lass uns das im Code anschauen. In Rust nutzen wir geschweifte Klammern { }, um einen “Raum” (einen sogenannten Geltungsbereich oder Scope) abzugrenzen.
fn main() {
// Hier beginnt ein neuer Raum (Scope)
{
// Wir erstellen ein Wort und speichern es in der Variable 'mein_buch'
let mein_buch = String::from("Die abenteuerliche Reise von Rusti");
// Die Variable 'mein_buch' ist jetzt der stolze Besitzer dieses Textes.
// Wir können das Buch lesen (auf dem Bildschirm ausgeben):
println!("Ich lese gerade: {}", mein_buch);
} // <-- Hier endet der Raum!
// Die Variable 'mein_buch' verlässt dieses Bereich und "hört auf zu existieren".
// Rust räumt den Speicherplatz, den der Text verbraucht hat, sofort und automatisch auf.
// Wenn wir JETZT versuchen würden, auf 'mein_buch' zuzugreifen:
// println!("{}", mein_buch); // Der Compiler würde schimpfen: "Dieses Buch gibt es hier nicht!"
}
Erklärung Zeile für Zeile:
fn main() { ... }: Das ist der Startpunkt unseres Rust-Programms. Jedes eigenständige Rust-Programm benötigt einemain-Funktion.{in Zeile 3: Dies öffnet einen neuen, inneren Geltungsbereich. Man kann sich das wie eine kleine Kiste oder ein Spielzimmer vorstellen.let mein_buch = String::from("...");in Zeile 5: Wir erschaffen einen Texttyp namensString. Im Gegensatz zu einfachen, festen Texten (Literalen) kann sich einStringzur Laufzeit verändern (länger oder kürzer werden). Dieser Text wird im dynamischen Arbeitsspeicher des Computers (dem sogenannten Heap) abgelegt. Die Variablemein_buchauf dem Stapelspeicher (Stack) wird nun als alleiniger Besitzer eingetragen.println!("...", mein_buch);in Zeile 9: Wir geben den Text auf der Konsole aus. Rust greift über den Besitzermein_buchauf den Text zu.}in Zeile 11: Dieser Raum wird geschlossen. Rust sieht: “Aha,mein_buchgeht jetzt verloren. Damein_buchder Besitzer des Texts ist, darf niemand anderes mehr darauf zugreifen. Ich lösche den Text jetzt aus dem Speicher.” Rust ruft hierfür im Hintergrund eine funktion namensdropauf, die den Speicherplatz sauber freigibt.- Nach der geschweiften Klammer existiert das Buch nicht mehr im Speicher des Computers.
2. Copy vs. Move
Was passiert eigentlich, wenn wir eine Variable einer anderen Variablen zuweisen? Hier kommt der größte Unterschied zu vielen anderen Programmiersprachen. In Rust gibt es zwei Wege: Kopieren (Copy) und Besitzübertragung (Move).
Die Rezept-Analogie (Copy)
Stell dir vor, du hast ein Rezept für die leckersten Waffeln der Welt auf einem Zettel aufgeschrieben. Ein Freund kommt vorbei und möchte das Rezept auch haben. Was machst du? Du nimmst einen Kopierer, machst ein Foto oder schreibst es auf einen neuen Zettel ab.
Jetzt habt ihr zwei separate Zettel mit demselben Rezept.
- Wenn dein Freund auf seinen Zettel kleckert oder Notizen macht, bleibt dein eigener Zettel sauber.
- Beide Zettel existieren unabhängig voneinander.
In Rust verhalten sich einfache Daten wie Zahlen, Buchstaben (char) oder Wahrheitswerte (bool) genau so. Sie belegen sehr wenig Speicherplatz auf dem extrem schnellen Stapelspeicher (Stack). Wenn wir sie einer neuen Variable zuweisen, werden sie einfach kopiert (Copy).
Schauen wir uns das im Code an:
fn main() {
let original_zahl = 42; // Eine einfache Zahl (vom Typ i32)
let kopierte_zahl = original_zahl; // Rust kopiert die Zahl bitweise
// Beide Variablen sind unabhängig voneinander gültig!
println!("Die original_zahl ist: {}", original_zahl);
println!("Die kopierte_zahl ist: {}", kopierte_zahl);
}
Erklärung Zeile für Zeile:
let original_zahl = 42;: Rust erstellt eine ganzzahlige Variable auf dem Stack. Da Zahlen eine feste Größe haben, implementieren sie standardmäßig den sogenanntenCopy-Trait (eine Eigenschaft, die dem Compiler sagt: “Mich darfst du einfach kopieren!”).let kopierte_zahl = original_zahl;: Weil Zahlen kopiert werden dürfen, wird der Wert42einfach im Speicher verdoppelt. Es gibt jetzt zwei getrennte Zahlen mit dem Wert42auf dem Stack.- Die Ausgaben zeigen, dass beide Variablen parallel benutzt werden können. Nichts wird ungültig.
Die Buch-Geschenk-Analogie (Move)
Stell dir nun vor, du hast keine Waffelrezept-Kopie, sondern dein einzigartiges, schweres Lieblings-Comicbuch. Es gibt weltweit nur dieses eine Exemplar. Ein Freund kommt vorbei und du schenkst es ihm. Du übergibst ihm das physische Buch.
Was passiert?
- Dein Freund ist jetzt der neue Besitzer des Buches.
- Dein eigenes Bücherregal ist an dieser Stelle leer.
- Wenn du am nächsten Tag in deinem Regal nach dem Buch greifst, um darin zu lesen, stellst du fest: Das Buch ist weg! Du kannst es nicht mehr lesen, weil du es weggegeben hast.
Genau das passiert in Rust mit komplexeren Daten wie dem String-Typ. Ein String kann riesig sein (z. B. ein ganzer Roman). Ihn einfach ungefragt zu kopieren, würde den Computer verlangsamen. Deshalb übergibt Rust stattdessen das Besitzrecht (Move).
Schauen wir uns den Code an, der den Compiler unglücklich macht:
fn main() {
// 1. Wir erstellen das Originalbuch im Speicher
let mein_original_buch = String::from("Rust für Entdecker");
// 2. MOVE! Wir übergeben das Buch an 'mein_freund'
let mein_freund = mein_original_buch;
// 3. Fehler-Versuch! Wir wollen das Buch selbst noch lesen:
// println!("Ich lese immer noch: {}", mein_original_buch);
}
Wenn du versuchst, diesen Code auszuführen, schlägt der Rust-Compiler sofort Alarm! Er gibt dir eine Fehlermeldung aus, die ungefähr so aussieht:
error[E0382]: borrow of moved value: `mein_original_buch`
Warum macht Rust das?
Wenn Rust den Besitz nicht übertragen würde, gäbe es zwei Variablen (mein_original_buch und mein_freund), die beide behaupten, das eine, echte Buch auf dem Heap zu besitzen. Wenn das Programm am Ende der main-Funktion ankommt, würden beide Variablen versuchen, das Buch im Speicher zu löschen (Deallokation). Das Löschen desselben Speichers durch zwei verschiedene Besitzer nennt man Double Free Error (doppelte Freigabe). Das kann zu schweren Sicherheitslücken führen. Rust verhindert das clever schon beim Kompilieren!
Was tun, wenn wir wirklich eine Kopie brauchen?
Wenn du das Buch wirklich kopieren möchtest (also ein exakt gleiches, zweites Buch drucken lassen willst, was etwas Zeit und Tinte kostet), kannst du die Methode .clone() (Klonen) benutzen:
fn main() {
let mein_original_buch = String::from("Rust für Entdecker");
// Wir klonen das Buch. Jetzt haben wir ein echtes Duplikat auf dem Heap!
let mein_freund = mein_original_buch.clone();
// Nun können beide ihr eigenes Buch lesen:
println!("Ich lese: {}", mein_original_buch);
println!("Mein Freund liest: {}", mein_freund);
}
3. Ausleihen (Referenzen)
Es wäre ziemlich anstrengend, wenn wir unsere Variablen jedes Mal verschenken oder klonen müssten, wenn wir sie nur kurz einer Funktion zeigen wollen. Stell dir vor, du möchtest deiner Oma dein Buch zeigen, damit sie die Seitenzahl abliest. Es wäre verrückt, ihr das Buch komplett zu schenken (Move) oder ein zweites Buch drucken zu lassen (Clone), nur damit sie kurz draufschauen kann.
Die Lösung: Du leihst ihr das Buch aus. Sie schaut kurz hinein und gibt es dir wieder zurück. In Rust nennen wir das Referenzen (oder Borrowing).
Wir erstellen eine Referenz, indem wir ein Und-Zeichen (&) vor den Namen der Variable setzen.
Unveränderliches Ausleihen (&)
Die Grundregel beim normalen Ausleihen eines Buches ist: Schauen ist erlaubt, Bemalen ist verboten! Dein Freund darf das Buch lesen, aber er darf keine Notizen hineinschreiben. Das ist eine unveränderliche Referenz (auch Lese-Referenz genannt).
fn main() {
let mein_buch = String::from("Die geheime Programmiersprache");
// Wir leihen das Buch an zwei Freunde gleichzeitig aus.
// Das '&' bedeutet: "Hier ist nur ein Blick auf das Buch, nicht das Buch selbst!"
let leser1 = &mein_buch;
let leser2 = &mein_buch;
// Beide dürfen das Buch zur gleichen Zeit lesen:
println!("Leser 1 sieht: {}", leser1);
println!("Leser 2 sieht: {}", leser2);
// Da wir das Buch nur verliehen haben, besitzen wir es immer noch selbst!
println!("Ich habe mein Buch noch: {}", mein_buch);
}
Erklärung Zeile für Zeile:
let mein_buch = String::from("...");: Wir erstellen den Text auf dem Heap.mein_buchis der Besitzer.let leser1 = &mein_buch;: Wir erstellen eine Referenz aufmein_buchund speichern sie inleser1. Der Typ vonleser1ist&String(Referenz auf einen String). Das ist wie ein Wegweiser, der auf das Buch zeigt.let leser2 = &mein_buch;: Wir erstellen eine zweite Referenz. Da das Buch nur gelesen wird, dürfen beliebig viele Leute gleichzeitig hineinschauen.- Am Ende des Programms verfallen die Leihverträge einfach. Da das Originalbuch immer bei uns (in der Variable
mein_buch) lag, wird es erst gelöscht, wennmein_bucham Ende dermain-Funktion den Scope verlässt.
Veränderliches Ausleihen (&mut) und die Erlaubnis zum Bemalen
Manchmal reicht das bloße Lesen nicht aus. Stell dir vor, du leihst deinem Freund ein Malbuch aus und erlaubst ihm explizit, ein Bild darin auszumalen.
Dafür müssen zwei Dinge gegeben sein:
- Das Buch muss überhaupt veränderlich sein (mit
let muterstellt). - Du musst eine veränderliche Referenz mit
&mutübergeben.
fn main() {
// 1. Das Buch MUSS veränderlich sein (mut)
let mut malbuch = String::from("Ein leeres Malbuch");
{
// 2. Wir leihen es veränderlich (&mut) an einen Zeichner aus
let zeichner = &mut malbuch;
// Der Zeichner malt ein Bild hinein (hängt Text an)
zeichner.push_str(" mit einer bunten Sonne!");
} // <-- Hier gibt der Zeichner das Buch zurück (zeichner-Referenz endet)
// Jetzt können wir das bemalte Buch wieder selbst anschauen:
println!("Das Malbuch enthält: {}", malbuch);
}
Erklärung Zeile für Zeile:
let mut malbuch = String::from("...");: Durch das Wörtchenmut(kurz für mutable, also veränderlich) erlauben wir überhaupt erst, dass der Inhalt dieses Strings später geändert werden darf.{ let zeichner = &mut malbuch; ... }: Wir öffnen einen kurzen Bereich und erstellen eine veränderliche Referenz&mut malbuch. Der Typ vonzeichnerist&mut String.zeichner.push_str("...");: Über die veränderliche Referenz fügen wir dem Original-String auf dem Heap neuen Text hinzu.- Nach dem inneren Scope endet die Lebensdauer von
zeichner. Das Buch wurde sicher an den Besitzer zurückgegeben.
Die goldene Regel des Ausleihens
Damit es nicht zu Chaos kommt, hat Rust eine ganz strenge Regel für das Ausleihen. Stell dir vor, ein Freund liest gerade in deinem Buch (unveränderliche Referenz). Im selben Moment kommt ein anderer Freund mit einem dicken Filzstift und kritzelt mitten auf die Seite, die der erste Freund gerade liest (veränderliche Referenz). Das gäbe ein Riesen-Chaos!
Deshalb gilt in Rust:
- Entweder du verleihst ein Buch an beliebig viele Leser gleichzeitig (
&), - Oder du verleihst es an genau einen Zeichner (
&mut), der alleine daran arbeitet. - Aber niemals beides gleichzeitig!
Schauen wir uns ein Beispiel an, bei dem der Compiler uns schützt:
fn main() {
let mut mein_buch = String::from("Detektivgeschichte");
// 1. Wir leihen das Buch zum Lesen aus
let leser = &mein_buch;
// 2. FEHLER! Wir versuchen, es gleichzeitig an jemanden zum Schreiben zu geben
// let schreiber = &mut mein_buch;
// Der Leser liest das Buch
println!("Der Leser liest gespannt: {}", leser);
}
Zusammenfassung der Begriffe für Einsteiger
| Begriff in Rust | Deutsche Bedeutung | Alltagsanalogie | Was passiert im Speicher? |
|---|---|---|---|
| Owner (Besitzer) | Der Eigentümer eines Werts. | Du besitzt dein Buch exklusiv. | Die Variable, die für das Löschen verantwortlich ist. |
| Move (Verschieben) | Besitzübergabe. | Du schenkst dein Buch einem Freund. | Der Zeiger auf den Speicher wird übertragen, alte Variable ungültig. |
| Copy (Kopieren) | Wert duplizieren. | Du kopierst ein kurzes Rezept auf einen neuen Zettel. | Der Wert wird bitweise im schnellen Stack-Speicher verdoppelt. |
Reference (&) | Unveränderliches Ausleihen. | Jemand darf in deinem Buch lesen, aber nicht hineinschreiben. | Ein Zeiger, der nur Lesezugriff erlaubt. |
Mutable Reference (&mut) | Veränderliches Ausleihen. | Jemand darf dein Buch ausleihen und darin zeichnen. | Ein exklusiver Zeiger, der Lese- und Schreibzugriff erlaubt. |