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:
-
Jeder Wert hat einen Besitzer
Ein Wert gehört immer einer Variablen, einem Feld oder einer Sammlung, die für ihn zuständig ist.
-
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.
-
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:
| Typ | Wofür | Geprüft |
|---|---|---|
| Box<T> | Ein Besitzer, Wert auf dem Heap | Beim Übersetzen |
| Rc<T> | Geteilter Besitz in einem Thread | Referenzzähler zur Laufzeit |
| Arc<T> | Geteilter Besitz über Threads | Atomarer Referenzzähler |
| RefCell<T> | Ändern trotz geteilter Referenz, ein Thread | Zur Laufzeit, Panic bei Verstoß |
| Mutex<T> | Ändern über Threads hinweg | Sperre 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
Ownership-Modell und was oft damit gleichgesetzt wird
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.
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.
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
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.
Kurse, die Ownership-Modell behandeln
Häufige Fragen
Ist clone eine Niederlage?
Was hat String und &str mit Ownership zu tun?
Verhindert Ownership alle Fehler in nebenläufigem Code?
Was bedeutet move vor einer Closure?
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.
Deine Ansprechpartner
Du willst das Thema nicht nur nachschlagen, sondern anwenden können? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Ordnet mit dir ein, welcher Kurs zu deinem Vorwissen passt.
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.