Ownership in Rust ist über drei formale Regeln definiert. Sie sind so kompakt, dass man sie in einem Satz zusammenfassen kann — und so mächtig, dass sie eine komplette Klasse von Speicher-Fehlern (Use-after-Free, Double-Free, Daten-Wettläufe) als Compile-Fehler erkennbar machen. Dieser Artikel formuliert die drei Regeln präzise, zeigt Schritt für Schritt mit Compiler-Output, wie der Borrow Checker sie anwendet, und erklärt, warum genau diese drei Regeln das Fundament der Sprache bilden.

Die drei Regeln

Aus dem offiziellen Rust Book — wörtlich:

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Klingt einfach. Wirkt komplex, sobald man Code schreibt. Wir gehen jede Regel einzeln durch.

Regel 1: Jeder Wert hat einen Besitzer

Ein Besitzer ist die Bindung (Variable, Struct-Feld, Vec-Element), in der ein Wert lebt. Beispiel:

Rust Wer besitzt was?
fn main() {
    let s = String::from("Hallo");
    // ^^^^ s ist der Besitzer des String-Werts.
    //      Der String selbst (Pointer, Länge, Capacity) lebt im Stack-Frame
    //      von main; die Bytes liegen auf dem Heap.
}

Im Beispiel:

  • Die Bindung s ist der Besitzer des String-Werts.
  • Der String-Wert selbst besteht aus drei Feldern (Pointer auf Heap, Länge, Kapazität) — diese drei Felder leben im Stack-Frame der Funktion main.
  • Die zugrundeliegenden Bytes („Hallo" = 5 Bytes) leben auf dem Heap.

Wenn s aus dem Scope verschwindet, ist der String-Wert nicht mehr erreichbar. Damit greift Regel 3.

Wer kann Besitzer sein?

  • Lokale Bindungen (let x = ...).
  • Funktions-Parameter (fn foo(x: String)x ist Besitzer im Funktions-Body).
  • Struct-Felder (struct Foo { feld: String } — das Struct besitzt den String).
  • Vec-, Array-, HashMap-Elemente (der Container besitzt die Elemente).
  • Tuple-Komponenten.

Nicht-Besitzer sind Referenzen (&T, &mut T) — sie zeigen auf etwas, was anderswo lebt. Mehr im Borrowing-Kapitel.

Regel 2: Es gibt nur einen Besitzer zur gleichen Zeit

Sobald ein Wert an eine neue Bindung übergeben wird, ist die alte Bindung nicht mehr der Besitzer. Bei non-Copy-Typen ist die alte Bindung sogar nicht mehr verwendbar:

Rust Besitz wandert (Move)
fn main() {
    let s1 = String::from("Hallo");
    let s2 = s1;                      // Besitz von s1 wandert zu s2
    // println!("{s1}");               // Fehler — s1 ist nicht mehr Besitzer
    println!("{s2}");                 // ok
}

Der Compiler-Fehler dazu:

Rust rustc-Diagnose
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:16
  |
2 |     let s1 = String::from("Hallo");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1}");
  |                ^^ value borrowed here after move

Drei wichtige Beobachtungen:

  • E0382 ist der häufigste Fehler-Code beim Lernen von Ownership.
  • Der Compiler zeigt exakt die Stelle, an der der Move passiert ist, und exakt die Stelle, an der der Move-after-Use auftritt.
  • String implementiert nicht Copy — daher Move. Bei Copy-Typen wie i32 würde stattdessen kopiert (siehe Copy-und-Clone-Artikel).

Warum keine zwei Besitzer?

Wenn s1 und s2 gleichzeitig denselben String besitzen würden, wem gehört der Heap-Speicher beim Drop? Würde der String-Speicher zweimal freigegeben werden (Double-Free), kämen klassische C-Probleme zurück. Mit „nur ein Besitzer" ist die Frage immer eindeutig: derjenige, der zuletzt den Wert besessen hat, gibt ihn frei.

Regel 3: Beim Scope-Ende wird gedroppt

Sobald der Besitzer einer Bindung den Scope verlässt — typischerweise mit der schließenden } einer Funktion oder eines Blocks — wird der Wert automatisch gedroppt. Drop heißt:

  • Bei einem String oder Vec: der Heap-Speicher wird freigegeben.
  • Bei einer File: der Datei-Handle wird geschlossen.
  • Bei einem MutexGuard: der Lock wird freigegeben.
  • Bei eigenen Typen mit Drop-Impl: die drop-Methode läuft.
Rust Drop am Scope-Ende
fn main() {
    {
        let s = String::from("Hallo");
        println!("{s}");
    }   // <-- s wird hier gedroppt, Heap-Speicher freigegeben.
    // println!("{s}");                 // Fehler — s existiert nicht mehr.
}

Die }-Klammer ist nicht nur syntaktisch — sie ist der Trigger für Drop. Genau das ist die Grundlage des RAII-Pattern (Resource Acquisition Is Initialization), das Rust aus C++ übernimmt: die Lebensdauer einer Ressource ist an die Lebensdauer einer Variable gebunden.

Drop-Reihenfolge

Bei mehreren Bindungen in einem Scope ist die Drop-Reihenfolge LIFO (Last In, First Out):

Rust Reihenfolge
struct Laut(&'static str);
impl Drop for Laut {
    fn drop(&mut self) {
        println!("Drop: {}", self.0);
    }
}

fn main() {
    let a = Laut("a");
    let b = Laut("b");
    let c = Laut("c");
    println!("--- Funktions-Ende ---");
}
// Ausgabe:
// --- Funktions-Ende ---
// Drop: c
// Drop: b
// Drop: a

c wurde zuletzt deklariert, also wird c zuerst gedroppt. Dann b, dann a. Das ist wichtig, wenn Ressourcen voneinander abhängen — etwa wenn b auf etwas in a zugreift, muss b zuerst weg sein.

Wie der Borrow Checker arbeitet

Der Borrow Checker ist die Phase im Rust-Compiler, die Ownership- und Borrow-Regeln zur Compile-Zeit prüft. Er liest deinen Code als Flow-Graph und verfolgt für jeden Wert:

  • Wo wird er erzeugt (welches let / welche Funktions-Rückgabe)?
  • Wo wird er gemoved (zu welcher anderen Bindung, in welche Funktion)?
  • Wo wird er gelesen oder geschrieben?
  • Wo verlässt sein Besitzer den Scope (Drop)?

Wenn irgendeine Operation gegen die Regeln verstößt — z. B. Lesen nach Move, Mutation während eines Shared-Borrows — gibt es einen Compile-Fehler.

Was der Borrow Checker nicht prüft

  • Logische Fehler (falsche Berechnung). Dafür sind Tests da.
  • Runtime-Panics (unwrap auf None, Index out of bounds). Der Borrow Checker garantiert Memory-Safety, nicht Korrektheit.
  • unsafe-Code. Innerhalb eines unsafe-Blocks darfst du Dinge tun, die der Borrow Checker normalerweise verbietet — auf eigene Verantwortung.

Was Ownership verhindert

Drei klassische Bug-Klassen aus C/C++, die in safe Rust per Konstruktion nicht möglich sind:

Use-after-Free

Rust Klassischer C-Bug — Rust verbietet
fn main() {
    let r;
    {
        let s = String::from("temporär");
        r = &s;
    }   // s wird hier gedroppt
    // println!("{r}");     // Würde auf freigegebenen Speicher zugreifen.
    // Der Compiler verbietet das mit E0597 (borrowed value does not live long enough).
}

In C wäre der Pointer r nach dem Free undefiniert — Lesen davon ist undefined behavior. In Rust kompiliert der Code gar nicht erst.

Double-Free

Rust Doppeltes Free — verhindert
fn main() {
    let s1 = String::from("Hallo");
    let s2 = s1;       // s1 nicht mehr Besitzer
    // Am Ende des Scopes wird nur s2 gedroppt — der Heap-Speicher
    // wird genau einmal freigegeben.
}

Da Regel 2 sicherstellt, dass es zu jedem Zeitpunkt nur einen Besitzer gibt, kann es keine zwei Drops auf denselben Wert geben.

Daten-Wettläufe

Beim Lesen/Schreiben zwischen Threads gilt: gleichzeitige &mut-Borrows sind ausgeschlossen, und der Send/Sync-Trait-Mechanismus erzwingt das auch über Thread-Grenzen hinweg. Details im Concurrency-Kapitel — die Grundlage ist die Single-Owner-Semantik.

Was passiert mit Copy-Typen?

Eine wichtige Ausnahme zu Regel 2: Copy-Typen werden nicht gemoved, sondern kopiert. Bei ihnen führt eine Zuweisung zu zwei unabhängigen Werten:

Rust Copy-Typen
fn main() {
    let x: i32 = 5;
    let y = x;             // x wird KOPIERT, nicht gemoved
    println!("{x} {y}");   // 5 5 — beide nutzbar
}

Welche Typen sind Copy? Alle Primitive (i32, f64, bool, char), Tupel aus Copy-Typen, Arrays aus Copy-Typen, alle Referenz-Typen &T. Nicht Copy sind alle Heap-allokierten Typen (String, Vec, Box), alle Drop-implementierenden Typen und alle &mut T-Referenzen.

Mehr im Copy-und-Clone-Artikel.

Praxis: Ownership-Regeln im echten Code

File-Handle korrekt schließen

Rust RAII mit Datei
use std::fs::File;
use std::io::Write;

fn schreibe_log(zeile: &str) -> std::io::Result<()> {
    let mut datei = File::create("/tmp/app.log")?;
    datei.write_all(zeile.as_bytes())?;
    datei.write_all(b"\n")?;
    Ok(())
}
// datei verlässt hier den Scope -> File-Handle wird automatisch geschlossen.

Du brauchst keinen close()-Aufruf. Der Drop-Impl von File schließt den Handle automatisch beim Scope-Ende. Selbst wenn write_all per ? mit einem Fehler aussteigt — der Drop läuft trotzdem.

Datenbank-Transaktion

Rust Transaktion mit RAII
struct Tx {
    committed: bool,
}

impl Tx {
    fn beginnen() -> Tx { Tx { committed: false } }
    fn commit(mut self) {
        self.committed = true;
        println!("COMMIT");
        // Drop läuft hier — wir sind committed
    }
}

impl Drop for Tx {
    fn drop(&mut self) {
        if !self.committed {
            println!("ROLLBACK");      // Nicht committed -> rollback
        }
    }
}

fn beispiel(soll_committen: bool) {
    let tx = Tx::beginnen();
    // ... Datenbank-Operationen ...
    if soll_committen {
        tx.commit();
    }
    // Ohne commit: tx wird hier gedroppt, Drop sieht !committed -> ROLLBACK
}

Klassisches RAII: die Transaktion hat genau einen Besitzer, beim Drop entscheidet sie selbst, was bei nicht-committed-State zu tun ist. Keine Möglichkeit, den Rollback zu vergessen.

Vec-Element verbrauchen

Rust Move aus Vec
fn main() {
    let v = vec![String::from("a"), String::from("b"), String::from("c")];
    let zweites = v[1].clone();        // Clone — v.clone() wäre teurer

    // let zweites = v[1];               // Fehler! Kann nicht aus Vec moven.
    // Lösung: into_iter() für verbrauchende Iteration, oder swap_remove.
}

Ein Vec besitzt seine Elemente. Direktes Move aus v[1] würde den Vec in einen inkonsistenten Zustand bringen — der Compiler verbietet das. Drei Auswege: .clone(), swap_remove(1) (tauscht mit letztem und entfernt), oder into_iter().

Builder-Pattern mit konsumierender finaler Methode

Rust Builder
struct ConfigBuilder {
    host: String,
    port: u16,
}

impl ConfigBuilder {
    fn neu() -> Self {
        ConfigBuilder { host: "localhost".into(), port: 8080 }
    }
    fn host(mut self, h: &str) -> Self { self.host = h.to_string(); self }
    fn port(mut self, p: u16) -> Self { self.port = p; self }
    fn bauen(self) -> Config {              // verbraucht den Builder
        Config { host: self.host, port: self.port }
    }
}

struct Config { host: String, port: u16 }

fn main() {
    let cfg = ConfigBuilder::neu()
        .host("api.example.com")
        .port(443)
        .bauen();
    println!("{}:{}", cfg.host, cfg.port);
}

bauen(self) verbraucht den Builder. Die Felder werden in den Config-Struct gemoved — keine Klone, keine Duplikate. Nach bauen() ist der Builder weg, der Config da.

Channel-Message senden

Rust Move durch Channel
use std::sync::mpsc::channel;
use std::thread;

fn main() {
    let (tx, rx) = channel::<String>();

    thread::spawn(move || {
        let nachricht = String::from("Hallo aus dem Thread");
        tx.send(nachricht).unwrap();    // nachricht wird in den Channel gemoved
        // println!("{nachricht}");      // Fehler — gemoved.
    });

    let empfangen = rx.recv().unwrap();
    println!("{empfangen}");
}

Beim Senden über einen Channel wird der Wert gemoved. Damit gibt es keinen geteilten Zustand zwischen Threads — der Empfänger ist der neue Besitzer.

Owner-Wechsel in einer HashMap

Rust HashMap-Insert
use std::collections::HashMap;

fn main() {
    let mut cache: HashMap<String, Vec<u8>> = HashMap::new();

    let key = String::from("user:42");
    let wert = vec![1, 2, 3, 4];

    cache.insert(key, wert);
    // println!("{key} {wert:?}");     // Fehler — beide gemoved in die Map.

    // Lookup mit &str — Map gibt Referenz, nicht Eigentum:
    if let Some(daten) = cache.get("user:42") {
        println!("{daten:?}");
    }
}

Wenn man Key und Value in eine HashMap einfügt, übernimmt die Map den Besitz. Lookup gibt Referenzen — die Daten bleiben in der Map.

Interessantes

Die drei Regeln sind alles, was Ownership ist.

Wer einen Bug versteht, fragt sich: gegen welche der drei Regeln verstößt mein Code? Fast jeder Ownership-Compiler-Fehler reduziert sich auf eine der drei Regeln — E0382 (Move + Use) ist Regel 2, E0597 (Use-after-Drop) ist Regel 3.

Besitzer ist immer eine Bindung, kein Wert.

Der Wert „lebt" — der Besitzer ist der Name, unter dem der Wert erreichbar ist. Nach einem Move existiert der Wert noch (jetzt unter einem anderen Namen), nur die alte Bindung ist „leer" — sie darf nicht mehr gelesen werden.

Move ist ein statisches Konzept — kein Runtime-Cost.

Bei let s2 = s1 werden im Maschinencode die drei Bytes (Pointer/Length/Capacity) von s1 zu s2 kopiert. Anschließend wird s1 vom Compiler als „leer" markiert, der Drop-Code für s1 nicht erzeugt. Im Maschinencode passiert nichts Magisches.

Drop läuft auch bei Panic.

Während eines Panics werden alle Werte im aktuellen Stack-Frame gedroppt, bevor der Panic weiter nach oben propagiert. Außer du nutzt panic = "abort" in Cargo.toml — dann wird der Prozess sofort beendet, ohne Drop-Aufrufe.

Copy-Typen umgehen Regel 2 — durch Kopieren.

let y = x; mit x: i32 ist kein Move, sondern eine Bit-Kopie. Beide sind danach unabhängig nutzbar. Copy ist ein Marker-Trait, den der Compiler interpretiert.

Move-Semantik ist die Standard-Semantik in Rust.

Anders als in C++, wo Kopie (oft tiefe Kopie) der Default ist und Move explizit angefordert wird. In Rust ist es umgekehrt: Move-by-Default, Kopie nur über Copy-Trait oder expliziten .clone()-Aufruf.

Der Borrow Checker ist deterministisch — Code kompiliert reproduzierbar.

Wenn dein Code heute kompiliert, kompiliert er auch morgen. Wenn er nicht kompiliert, geht's nicht mit „rustc nochmal versuchen". Die Regeln sind formal und werden gleich auf jedes Programm angewandt.

Ownership ist mehr als Memory-Management.

Die drei Regeln gelten auch für File-Handles, Mutex-Guards, Datenbank-Transaktionen, GPU-Buffer und jede andere Ressource. Eine Datei, ein MutexGuard oder ein eigener RAII-Wrapper wird genauso behandelt wie ein String.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Ownership

Zur Übersicht