Speichersicherheit im Compiler

Was ist das Ownership-Modell in Rust?

Ownership, Borrow-Checker

Das Ownership-Modell ist Rusts Regelwerk für die Speicherverwaltung: Jeder Wert hat genau einen Besitzer, und wenn dieser seinen Gültigkeitsbereich verlässt, wird der Wert freigegeben. Der Compiler prüft die Regeln beim Übersetzen und weist Code zurück, der sie verletzt, ganz ohne Garbage Collector.

Rust hat keinen Garbage Collector und trotzdem keine baumelnden Zeiger, weil dir der Compiler vorher sagt, an welcher Stelle dein Programm Speicher falsch benutzt.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Grundregel
Jeder Wert hat genau einen Besitzer
Prüft
Der Borrow-Checker beim Übersetzen
Ausleihen
Viele lesend oder genau einer schreibend
Verhindert
Use-after-free, doppelte Freigabe, Data Races

Die drei Ownership-Regeln

Das offizielle Rust-Buch fasst das Modell in drei Sätzen zusammen. Alles Weitere, Move, Ausleihen und Lebensdauern, folgt aus ihnen:

  1. Jeder Wert hat einen Besitzer

    Ein Wert gehört immer einer Variablen, einem Feld oder einer Sammlung, die für ihn zuständig ist.

  2. Es gibt immer nur einen Besitzer

    Zwei Variablen können nicht gleichzeitig Eigentümer desselben Werts sein. Wechselt die Zuständigkeit, ist die alte Variable ungültig.

  3. Endet der Besitzer, endet der Wert

    Verlässt der Besitzer seinen Gültigkeitsbereich, ruft Rust automatisch drop auf und gibt den Speicher frei, genau einmal.

Die Freigabe am Ende des Gültigkeitsbereichs gilt nicht nur für Speicher. Dateien, Netzwerkverbindungen und Sperren hängen in Rust ebenfalls an einem Besitzer und werden mit ihm geschlossen, ohne dass ein close im Code stehen muss.

Move: wenn der Besitz wandert

Wenn du einen Wert einer anderen Variablen zuweist oder an eine Funktion übergibst, wandert der Besitz mit. Die alte Variable ist danach nicht mehr benutzbar, der Compiler nennt das moved. Bei einem String wird dabei nichts kopiert, nur die Zuständigkeit für den Speicher auf dem Heap wechselt:

let s1 = String::from("Hallo");
let s2 = s1;            // Besitz wandert von s1 zu s2
println!("{s1}");       // error[E0382]: borrow of moved value: `s1`

Einfache Typen wie Ganzzahlen, Gleitkommazahlen und bool verhalten sich anders. Sie implementieren den Trait Copy und werden bei der Zuweisung bitweise kopiert, beide Variablen bleiben gültig. Für eigene Typen steuerst du das über Copy und Clone, wobei eine tiefe Kopie immer ausdrücklich als clone() im Code steht. Genau deshalb siehst du in Rust, wo kopiert wird, und musst es nicht erraten.

Ausleihen und der Borrow-Checker

Damit nicht ständig Besitz hin- und hergereicht wird, gibt es Referenzen, in Rust Ausleihen (Borrowing) genannt. Die Regel dahinter: Entweder beliebig viele lesende Referenzen (&T) oder genau eine schreibende (&mut T), nie beides gleichzeitig. Dazu muss jede Referenz kürzer leben als der Wert, auf den sie zeigt. Der Borrow-Checker prüft beides beim Übersetzen:

let mut liste = vec![1, 2, 3];
let erstes = &liste[0];   // lesende Ausleihe
liste.push(4);            // error[E0502]: cannot borrow `liste` as mutable
                          // because it is also borrowed as immutable
println!("{erstes}");

Das Beispiel zeigt, welchen Fehler die Regel verhindert. push kann den Vektor an eine neue Stelle im Speicher verschieben, wenn der Platz nicht mehr reicht. Die Referenz erstes würde dann auf freigegebenen Speicher zeigen, in C++ ein klassischer, schwer zu findender Absturz. Rust baut das Programm gar nicht erst.

Wie lange eine Referenz gültig bleiben muss, beschreiben Lebensdauern (Lifetimes). In den meisten Fällen leitet der Compiler sie selbst ab, eine Angabe wie 'a brauchst du erst, wenn eine Funktion mehrere Referenzen bekommt und nicht eindeutig ist, an welcher die Rückgabe hängt.

Geteilter Besitz, wenn er wirklich nötig ist

Manche Datenstrukturen haben keinen natürlichen einzelnen Besitzer, etwa ein Graph oder ein Zustand, den mehrere Threads lesen. Dafür bringt die Standardbibliothek Typen mit, die einen Teil der Prüfung bewusst in die Laufzeit verschieben:

TypWofürGeprüft
Box<T>Ein Besitzer, Wert auf dem HeapBeim Übersetzen
Rc<T>Geteilter Besitz in einem ThreadReferenzzähler zur Laufzeit
Arc<T>Geteilter Besitz über ThreadsAtomarer Referenzzähler
RefCell<T>Ändern trotz geteilter Referenz, ein ThreadZur Laufzeit, Panic bei Verstoß
Mutex<T>Ändern über Threads hinwegSperre zur Laufzeit

Typische Kombinationen sind Rc<RefCell<T>> in einem Thread und Arc<Mutex<T>> über Threads. Sie sind kein Notausgang, aber auch nicht der erste Griff: Wer sie überall einsetzt, hat meist ein Datenmodell aus einer anderen Sprache übernommen, in dem viele Stellen dieselbe Struktur ändern.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Nicht verwechseln

Ownership-Modell und was oft damit gleichgesetzt wird

Ownership-Modell gegen Garbage Collection

Eine Laufzeitumgebung wie in Java, C# oder Go räumt unbenutzte Objekte weg, wann sie es für richtig hält. Rust legt den Freigabezeitpunkt anhand der Gültigkeitsbereiche schon beim Übersetzen fest, dafür gibt es keine Pausen durch einen Aufräumvorgang.

Ownership-Modell gegen RAII in C++

Die Idee, Ressourcen an die Lebensdauer eines Objekts zu binden, ist dieselbe. C++ verlässt sich aber darauf, dass niemand eine Referenz auf ein bereits zerstörtes Objekt behält. Rust prüft genau das und lehnt den Code sonst ab.

Ownership-Modell gegen Unsafe

In einem unsafe-Block darfst du Dinge tun, die der Compiler nicht prüfen kann, etwa rohe Zeiger dereferenzieren. Die Ownership-Regeln für normale Referenzen gelten dort weiter, nur die Beweislast für die zusätzlichen Operationen liegt bei dir.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
In der Praxis

Der Compiler streitet früh, dafür fehlt der Absturz später

Wer aus C oder C++ kommt, kennt die Fehlerklasse: Ein Zeiger verweist auf Speicher, der längst freigegeben ist, und das Programm fällt irgendwann an einer ganz anderen Stelle um. Rust verlegt diese Diskussion in die Übersetzung. Der fragliche Code läuft nicht erst und bricht dann zusammen, er wird gar nicht erst gebaut.

Die erste Woche fühlt sich trotzdem zäh an, in der Rust-Welt heißt das Kampf mit dem Borrow-Checker. Er lehnt Code ab, der in anderen Sprachen selbstverständlich ist, etwa wenn du eine Liste durchläufst und dabei Elemente entfernst. Die üblichen Auswege sind Klonen, wo es billig ist, Indizes statt Referenzen, das Sammeln von Änderungen in einem zweiten Durchlauf oder eine klare Trennung, welche Struktur wem gehört.

Für die Einordnung wichtig: Die Prüfung kostet zur Laufzeit nichts. Es gibt keinen Sammler, der das Programm zwischendurch anhält, und keine Referenzzähler, außer du forderst sie mit Rc oder Arc ausdrücklich an. Genau deshalb taucht Rust dort auf, wo unvorhersehbare Pausen stören, also in Systemnähe, in Netzwerkdiensten und auf eingebetteter Hardware.

Ownership-Modell lernen

Wo der Borrow-Checker im Alltag wirklich zuschlägt und wie du dein Datenmodell darauf zuschneidest, üben die Rust-Trainings für Entwicklerteams an eigenen Beispielen.

Häufige Fragen

Ist clone eine Niederlage?
Nein. Eine Kopie an einer unkritischen Stelle ist besser als ein verbogenes Datenmodell. Beim Lernen ist clone ein legitimes Mittel, um voranzukommen, und du kannst später gezielt die Stellen aufräumen, die in einer heißen Schleife liegen.
Was hat String und &str mit Ownership zu tun?
String besitzt seinen Text auf dem Heap und gibt ihn am Ende seines Gültigkeitsbereichs frei. &str ist nur eine geliehene Sicht auf Text, der jemand anderem gehört. Funktionen, die Text nur lesen, nehmen deshalb meist &str entgegen und funktionieren so mit beiden.
Verhindert Ownership alle Fehler in nebenläufigem Code?
Nein, aber eine wichtige Klasse. Weil zwei Threads nicht gleichzeitig schreibend auf denselben Wert zugreifen können, sind Data Races in sicherem Rust ausgeschlossen. Deadlocks oder logische Wettlaufsituationen, etwa eine falsche Reihenfolge von Ereignissen, kann der Compiler dagegen nicht erkennen.
Was bedeutet move vor einer Closure?
Mit move übernimmt die Closure den Besitz der Variablen, die sie benutzt, statt sie nur auszuleihen. Das ist nötig, wenn die Closure länger lebt als die aktuelle Funktion, typischerweise bei thread::spawn.

Passt thematisch dazu

Wenn der Compiler deinen Entwurf zurückweist, wiederholen sich die Meldungen, die dir am Anfang am häufigsten begegnen, und zu jeder gehört ein Umbau, der länger hält als der schnelle Trick.

Wer aus C kommt, kennt die Fehlerklasse, vor der dich der Compiler hier bewahrt, und weiß, wie mühsam sich derselbe Fehler in C erst nach dem Absturz einkreisen lässt.

Persönlich für dich da

Deine Ansprechpartner

Du willst das Thema nicht nur nachschlagen, sondern anwenden können? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Ordnet mit dir ein, welcher Kurs zu deinem Vorwissen passt.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant Inhouse-Trainings, die an euren eigenen Daten und Abläufen ansetzen.

Ownership-Modell im Kurs statt im Lexikon

Nachschlagen bringt dich bis zum Verstehen. Anwenden lernst du an echten Aufgaben.