Der Drop-Trait ist das, was Ownership operativ macht: er definiert, wie ein Wert freigegeben wird, sobald sein Besitzer den Scope verlässt. Anders als bei Garbage Collection passiert das deterministisch zur Compile-Zeit-festgelegten Stelle. File-Handles werden zuverlässig geschlossen, Mutex-Guards freigegeben, Datenbank-Transaktionen committed oder rollbacked — alles ohne expliziten Cleanup-Code im Aufrufer. Dieses Pattern heißt RAII (Resource Acquisition Is Initialization) und ist eines der mächtigsten Werkzeuge der Sprache.

Der Drop-Trait

Aus der Stdlib (vereinfacht):

Rust Drop-Definition
pub trait Drop {
    fn drop(&mut self);
}

Ein Typ, der Drop implementiert, gibt eine Cleanup-Methode an. Der Compiler ruft sie automatisch beim Scope-Ende auf.

Rust Eigene Drop-Impl
struct Verbindung {
    name: String,
}

impl Drop for Verbindung {
    fn drop(&mut self) {
        println!("Schließe Verbindung: {}", self.name);
    }
}

fn main() {
    let v = Verbindung { name: "API".into() };
    println!("Arbeite mit {}", v.name);
}
// Ausgabe:
// Arbeite mit API
// Schließe Verbindung: API

Die drop-Methode läuft beim Funktions-Ende automatisch. Du musst sie nicht manuell aufrufen — der Compiler erkennt den Scope-Übergang und fügt den Aufruf ein.

Wann genau läuft drop?

Drop wird in folgenden Situationen ausgelöst:

Am Scope-Ende

Rust Block-Scope
fn main() {
    {
        let v = Verbindung { name: "Inner".into() };
    }   // <-- v.drop() läuft hier
    println!("Nach dem Block");
}
# struct Verbindung { name: String }
# impl Drop for Verbindung { fn drop(&mut self) { println!("Drop: {}", self.name); } }

Jede }-Klammer, die einen Scope schließt, droppt alle ihre lokalen non-Copy-Bindungen.

Beim Funktions-Return

Rust Return-Drop
fn main() {
    let v = Verbindung { name: "Funktion".into() };
    return;
    // Auch wenn return frühzeitig — v wird gedroppt.
}
# struct Verbindung { name: String }
# impl Drop for Verbindung { fn drop(&mut self) { println!("Drop: {}", self.name); } }

Bei Move in eine andere Funktion

Rust Move-Drop
fn nehmen(v: Verbindung) {
    println!("nehmen: {}", v.name);
}   // <-- v wird hier gedroppt

fn main() {
    let v = Verbindung { name: "Param".into() };
    nehmen(v);              // Move passt Besitz weiter
    println!("zurück in main");
}
# struct Verbindung { name: String }
# impl Drop for Verbindung { fn drop(&mut self) { println!("Drop: {}", self.name); } }

v wird in nehmen gemoved, und droppt im Funktions-Scope von nehmen, nicht in main.

Bei Panic (außer mit panic = abort)

Bei einem Panic werden alle lokalen Bindungen des aktuellen Stack-Frames gedroppt, bevor der Panic in den nächsthöheren Frame propagiert. So bleibt das Programm im Sinne von Ressourcen-Cleanup konsistent.

Rust Panic + Drop
fn main() {
    let v = Verbindung { name: "Panic-Test".into() };
    panic!("Etwas ist schiefgelaufen");
    // v wird trotzdem gedroppt, bevor der Panic Stack-unwinds.
}
# struct Verbindung { name: String }
# impl Drop for Verbindung { fn drop(&mut self) { println!("Drop: {}", self.name); } }

In Cargo.toml mit [profile.release] panic = "abort" wird der Prozess sofort beendet, ohne Drop-Aufrufe — eine bewusste Wahl für maximale Performance.

Bei expliziten Aufruf von std::mem::drop

Rust Explizites Drop
fn main() {
    let v = Verbindung { name: "Manuell".into() };
    std::mem::drop(v);          // Sofortiges Drop
    println!("Nach drop");
    // println!("{}", v.name);   // Fehler — v ist weg.
}
# struct Verbindung { name: String }
# impl Drop for Verbindung { fn drop(&mut self) { println!("Drop: {}", self.name); } }

std::mem::drop ist nicht direkt der Drop::drop-Methode — es ist eine kleine Funktion, die den Wert konsumiert und sich darauf verlässt, dass der Compiler ihn am Funktions-Ende droppt. Trick: die Funktion ist trivial:

Rust std::mem::drop-Implementierung
pub fn drop<T>(_x: T) {}
// Der Parameter _x wird in drop gemoved und am Funktions-Ende gedroppt.

Drop-Reihenfolge: LIFO

Bei mehreren Werten in einem Scope werden sie in umgekehrter Deklarations-Reihenfolge gedroppt:

Rust LIFO-Drop
struct Laut(String);
impl Drop for Laut {
    fn drop(&mut self) {
        println!("Drop: {}", self.0);
    }
}

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

Das ist wichtig, wenn Ressourcen aufeinander aufbauen: c zuerst, dann b, dann a — wenn c z. B. ein File-Handle ist, das in einen Buffer in b schreibt, sollte c zuerst weg sein.

Struct-Felder: Deklarations-Reihenfolge

Bei einem Drop-Impl auf einem Struct werden die Felder in Deklarations-Reihenfolge gedroppt, nachdem das drop-Methoden-Body gelaufen ist:

Rust Struct-Drop
struct Container {
    erstes: Laut,
    zweites: Laut,
}

impl Drop for Container {
    fn drop(&mut self) {
        println!("Container::drop läuft (vor Feld-Drops)");
    }
}

# struct Laut(String);
# impl Drop for Laut { fn drop(&mut self) { println!("Drop: {}", self.0); } }

fn main() {
    let _c = Container {
        erstes: Laut(String::from("a")),
        zweites: Laut(String::from("b")),
    };
    // Reihenfolge beim Drop:
    // 1. Container::drop()
    // 2. erstes (a)
    // 3. zweites (b)
}

Erst die eigene drop-Methode des Outer-Structs, dann die Felder in Deklarations-Reihenfolge.

Drop ist nicht aufrufbar

Eine wichtige Subtilität: du kannst drop als Methode nicht direkt aufrufen:

Rust Manuelles drop verboten
# struct Verbindung;
# impl Drop for Verbindung { fn drop(&mut self) {} }
fn main() {
    let v = Verbindung;
    // v.drop();          // Fehler! Explicit use of destructor method.
    std::mem::drop(v);     // OK — geht über die std::mem::drop-Funktion
}

Der Grund: würde v.drop() direkt erlaubt sein, könnte der Wert hinterher noch verwendet werden — der Compiler weiß ja nicht, dass drop „besonders" ist. Mit std::mem::drop(v) wird v in die Funktion gemoved, und der Compiler sieht das als regulären Move — danach ist v weg.

std::mem::forget — Drop überspringen

Es gibt einen seltsamen, aber legalen Weg, Drop zu verhindern: std::mem::forget.

Rust forget
# struct Verbindung;
# impl Drop for Verbindung { fn drop(&mut self) { println!("Drop läuft"); } }
fn main() {
    let v = Verbindung;
    std::mem::forget(v);        // v wird NICHT gedroppt.
    println!("Nach forget");
}
// Ausgabe:
// Nach forget
// (kein „Drop läuft"!)

forget verhindert den Drop, ohne den Speicher freizugeben. Wichtig:

  • Es ist safe Rust — kein unsafe nötig.
  • Es ist ein Memory-Leak — die Heap-Allocations (falls vorhanden) werden nie freigegeben.
  • Anwendungsfälle: FFI mit C-Code, der Ownership übernimmt. Manuelle Speicher-Manipulation in Unsafe-Wrappers.

Memory-Leaks sind in Rust safe im Borrow-Checker-Sinne — sie verletzen keine der drei Ownership-Regeln. Sie sind „nur" ein logischer Bug.

Drop und Move

Wichtig: nach einem Move wird nicht gedroppt — der Wert ist ja jetzt woanders.

Rust Move + Drop
# struct Laut(String);
# impl Drop for Laut { fn drop(&mut self) { println!("Drop: {}", self.0); } }
fn nehmen(_l: Laut) {
    println!("In nehmen");
}   // _l wird hier gedroppt

fn main() {
    let l = Laut(String::from("x"));
    nehmen(l);           // l wird in nehmen gemoved
    println!("Nach nehmen");
    // l ist hier nicht mehr nutzbar — und es wird auch nicht erneut gedroppt.
}
// Ausgabe:
// In nehmen
// Drop: x
// Nach nehmen

Der Compiler verfolgt pro Bindung, ob sie noch besessen wird. Eine gemovedte Bindung wird nicht am Scope-Ende erneut gedroppt — das wäre Double-Free.

Praxis: Drop im echten Code

Die folgenden Beispiele zeigen RAII-Patterns aus der Realität — alle nutzen denselben Mechanismus: ein Typ, der Drop implementiert, sichert Cleanup zu, egal wie der Code-Pfad endet (regulär, mit ?, mit Panic). Diese Robustheit ist es, die Rust-Code so produktiv für Ressourcen-Management macht.

File-Handle automatisch schließen

Rust File-Drop
use std::fs::File;
use std::io::Write;

fn schreibe_log(zeile: &str) -> std::io::Result<()> {
    let mut datei = File::create("/tmp/app.log")?;
    writeln!(datei, "{zeile}")?;
    Ok(())
}
// datei wird hier gedroppt → File-Handle wird OS-seitig geschlossen.

Das ist der absolute Klassiker für RAII: die Stdlib-File-Implementierung hat ein Drop, das den OS-Filedescriptor mit close(2) freigibt. Du brauchst nirgendwo eine explizite file.close()-Zeile — der Compiler garantiert, dass Drop am Scope-Ende läuft.

Was diese Garantie besonders wertvoll macht: sie hält auch bei vorzeitigem Return. Wenn File::create einen Fehler liefert, fängt ? ihn ab und kehrt zurück — aber Drop für eventuell bereits initialisierte Werte läuft trotzdem. Wenn writeln! mit einem I/O-Fehler scheitert, gilt dasselbe. Du kannst die Funktion an beliebiger Stelle verlassen, der File-Handle wird in jedem Fall geschlossen. In C wäre das mit goto cleanup und einer Reihe von Initialisierungs-Flags zu lösen; in Rust ist es transparent.

Mutex-Guard

Rust Lock-Drop
use std::sync::Mutex;

fn main() {
    let counter = Mutex::new(0);

    {
        let mut guard = counter.lock().unwrap();
        *guard += 1;
    }   // guard wird hier gedroppt → Mutex freigegeben

    // Anderer Code kann jetzt counter locken
    let again = counter.lock().unwrap();
    println!("{}", *again);
}

Der MutexGuard ist ein RAII-Wrapper um einen gehaltenen Lock. Solange der Guard lebt, gilt der Mutex als gesperrt; sobald er gedroppt wird, gibt sein Drop-Impl den Lock frei und macht den Mutex für andere Threads zugänglich. Die geschwungenen Klammern um die Lock-Operation sind das wichtigste Idiom: sie definieren das Scope, in dem der Lock gehalten wird, und garantieren, dass er pünktlich wieder freigegeben wird.

In anderen Sprachen ist „den Lock vergessen freizugeben" eine der häufigsten Concurrency-Bug-Quellen. In Rust ist das praktisch unmöglich — wenn du den Guard nicht explizit am Leben hältst, wird er automatisch gedroppt, und der Lock geht zurück an die nächste Wartende. Der Compiler übernimmt diese Disziplin für dich.

Datenbank-Transaktion mit Rollback-on-Drop

Rust DB-Transaktion
struct Tx {
    id: u64,
    commited: bool,
}

impl Tx {
    fn beginnen() -> Self {
        println!("BEGIN tx={}", 42);
        Tx { id: 42, commited: false }
    }
    fn commit(mut self) {
        self.commited = true;
        println!("COMMIT tx={}", self.id);
    }
}

impl Drop for Tx {
    fn drop(&mut self) {
        if !self.commited {
            println!("ROLLBACK tx={}", self.id);
        }
    }
}

fn beispiel(soll_committen: bool) {
    let tx = Tx::beginnen();
    // ... Datenbank-Operationen ...
    if soll_committen {
        tx.commit();
    }
    // Wenn nicht committed: tx.drop() läuft -> ROLLBACK
}

Das Datenbank-Transaktions-Pattern ist eines der elegantesten Anwendungen von Drop. Die Transaktion startet beim beginnen-Aufruf, und das commited-Flag dokumentiert ihren Zustand. Die commit-Methode setzt das Flag auf true und nimmt den Tx-Wert per mut self entgegen — danach läuft das normale Drop, das aber dank des Flags nichts mehr tut.

Wenn der Code irgendwo zwischen beginnen und commit mit ? aussteigt (etwa weil eine Insert-Operation einen Constraint verletzt), oder wenn ein Panic aus tieferem Code propagiert, läuft Drop trotzdem — und sieht das ungesetzte committed-Flag, woraufhin er das ROLLBACK ausgibt. Eine Transaktion kann auf diese Weise nicht „vergessen" werden, weil der Compiler den korrekten Cleanup erzwingt.

Dieses Pattern findest du in produktiven Datenbank-Bibliotheken wie sqlx, diesel und rusqlite — alle nutzen RAII für Transaktions-Management, ergänzt um optionale explizite commit()/rollback()-Methoden für Code, der die Entscheidung dynamisch treffen will.

Scope-Guard für temporäre State-Änderungen

Rust Scope-Guard
// Vereinfachte Variante mit einer einzelnen globalen Variable.
// Realistisch nutzt man dafür ein Borrow auf ein gemeinsames Counter-
// Objekt — siehe Borrowing- und Interior-Mutability-Kapitel.
static mut INDENT_LEVEL: u32 = 0;

struct Indenter;

impl Indenter {
    fn neu() -> Self {
        unsafe { INDENT_LEVEL += 1; }
        Indenter
    }
}

impl Drop for Indenter {
    fn drop(&mut self) {
        unsafe { INDENT_LEVEL -= 1; }
    }
}

fn level() -> u32 { unsafe { INDENT_LEVEL } }

fn main() {
    {
        let _i = Indenter::neu();
        assert_eq!(level(), 1);
        {
            let _j = Indenter::neu();
            assert_eq!(level(), 2);
        }
        assert_eq!(level(), 1);
    }
    assert_eq!(level(), 0);
}

Das Scope-Guard-Pattern verallgemeinert die Idee „mache X jetzt, mache es rückgängig später": der Konstruktor erhöht den Level, das Drop senkt ihn wieder. Verschachtelte Code-Blöcke bekommen automatisch eine Schachtelungsebene, ohne dass der Aufrufer manuell Increment/Decrement-Code schreiben muss. Anwendungen: Logging-Context, Tracing-Spans, Render-Tree-Tiefe, AST-Visitor-Depth.

Wir verwenden hier eine static mut-Variable plus unsafe, damit das Beispiel ohne weitere Konzepte auskommt. In produktivem Code würde man das Counter-Objekt per Referenz teilen — das benötigt aber Borrowing und Interior Mutability, die wir in späteren Kapiteln einführen.

Cleanup nach kritischen Operations

Rust Backup-Wiederherstellen
struct BackupRestore {
    original: Vec<u8>,
    target: std::path::PathBuf,
    committed: bool,
}

impl BackupRestore {
    fn neu(pfad: std::path::PathBuf) -> std::io::Result<Self> {
        let original = std::fs::read(&pfad)?;
        Ok(BackupRestore { original, target: pfad, committed: false })
    }
    fn commit(mut self) {
        self.committed = true;
        // Drop läuft, aber tut nichts mehr.
    }
}

impl Drop for BackupRestore {
    fn drop(&mut self) {
        if !self.committed {
            // Bei Panic / Fehler: alten Inhalt wiederherstellen.
            let _ = std::fs::write(&self.target, &self.original);
        }
    }
}

Ein Backup-on-Failure-Pattern: vor dem Schreiben den aktuellen Datei-Inhalt sichern, dann modifizieren, dann bei Erfolg explizit committen. Wenn etwas zwischendurch schiefgeht — sei es ein I/O-Fehler oder ein Panic in tieferem Code —, läuft Drop und schreibt den gesicherten Original-Inhalt zurück.

Das Pattern ist die typische Antwort auf „Wie machen wir Schreib-Operationen sicher gegen Crashs?". Mit RAII bekommst du die Rollback-Garantie ohne explizite try/catch-Disziplin: der Wiederherstellungs-Code läuft automatisch, ohne dass der Aufrufer ihn aufrufen muss.

Eine kleine Subtilität: das let _ = std::fs::write(...) im Drop ignoriert bewusst Fehler. Wenn auch das Wiederherstellen scheitert (z. B. weil der Disk voll ist), kann Drop nichts dagegen tun — es darf keinen Fehler zurückgeben und keinen Panic auslösen. In produktivem Code würde man hier einen Log-Eintrag schreiben.

Logging-Span (tracing-Pattern)

Rust Tracing-Span
struct Span { name: String, start: std::time::Instant }

impl Span {
    fn neu(name: impl Into<String>) -> Self {
        let name = name.into();
        println!(">>> {name}");
        Span { name, start: std::time::Instant::now() }
    }
}

impl Drop for Span {
    fn drop(&mut self) {
        let ms = self.start.elapsed().as_millis();
        println!("<<< {} ({}ms)", self.name, ms);
    }
}

fn berechnen() {
    let _s = Span::neu("berechnen");
    // ... Arbeit ...
    std::thread::sleep(std::time::Duration::from_millis(10));
}
// Beim Funktions-Ende: Span::drop läuft, gibt Latenz aus.

Ein Span misst die Dauer einer Operation per RAII — und das Pattern ist die Grundlage moderner Tracing-Frameworks wie tracing und opentelemetry. Der Konstruktor logged den Start, das Drop logged das Ende mit der gemessenen Dauer. Der Funktions-Body braucht nur eine Zeile (let _s = Span::neu(...)), und du bekommst automatisch Start-Log, End-Log und Latenz-Messung — ohne expliziten Cleanup-Code.

Das _s als Name ist Konvention für „ich brauche den Wert nicht, aber er soll am Leben bleiben". Ein _ ohne Namen würde sofort gedroppt — was hier den ganzen Sinn zunichte machen würde, denn dann läge Start-Log und End-Log direkt aufeinander. Wer in einem Reviewer-Kommentar mal „warum heißt die Variable _s?" sieht: genau aus diesem Grund.

Buffered Writer mit Auto-Flush

Rust BufWriter-Pattern
use std::io::{BufWriter, Write};
use std::fs::File;

fn schreibe_viel() -> std::io::Result<()> {
    let file = File::create("/tmp/big.log")?;
    let mut buf = BufWriter::new(file);

    for i in 0..1_000 {
        writeln!(buf, "Zeile {i}")?;
    }
    Ok(())
}
// buf wird hier gedroppt → noch nicht geschriebene Bytes werden geflusht.
// Dann wird file gedroppt → Handle geschlossen.

BufWriter puffert Schreibzugriffe in einem internen Buffer und schreibt erst dann tatsächlich in die unterliegende Datei, wenn der Buffer voll ist oder explizit ein flush() aufgerufen wird. Sein Drop-Impl ruft flush() selbst auf — die Pufferung ist also transparent: du musst nichts manuell tun, und am Ende deines Scope sind alle Bytes geschrieben.

Aber: das ist eine der wenigen Stellen, wo Drop subtil tückisch ist. Wenn flush() im Drop scheitert (etwa weil die Disk voll ist oder die Verbindung abreißt), wird der Fehler ignoriert — der Drop hat keine Möglichkeit, ihn zurückzugeben. Für unkritische Logs ist das ok; für Daten, deren erfolgreiches Schreiben relevant ist (Finanztransaktionen, Audit-Logs, Backup-Snapshots), solltest du buf.flush()? explizit aufrufen, bevor der Scope endet. Dann fängst du den Fehler ab, statt ihn zu verschlucken.

Performance-Spike-Detector

Rust Threshold-Warning
struct SlowWarning {
    label: String,
    start: std::time::Instant,
    threshold_ms: u128,
}

impl SlowWarning {
    fn neu(label: impl Into<String>, ms: u128) -> Self {
        SlowWarning { label: label.into(), start: std::time::Instant::now(), threshold_ms: ms }
    }
}

impl Drop for SlowWarning {
    fn drop(&mut self) {
        let elapsed = self.start.elapsed().as_millis();
        if elapsed > self.threshold_ms {
            eprintln!("SLOW: {} took {}ms (threshold {}ms)",
                      self.label, elapsed, self.threshold_ms);
        }
    }
}

fn arbeite() {
    let _w = SlowWarning::neu("arbeite", 50);
    // ... Arbeit ...
}

Ein pragmatisches Performance-Tooling, das mit RAII trivial wird: jede Funktion, die einen SlowWarning anlegt, bekommt automatisch eine Stopwatch und gibt eine Warnung aus, wenn sie über dem Threshold lag. Du musst keinen Profiler einbauen, keinen Tracer initialisieren — eine Zeile am Anfang der Funktion reicht.

Im Zusammenspiel mit cfg! und Feature-Flags kann man das im Release-Build sogar komplett wegoptimieren: ein cfg(feature = "slow-warnings") um den SlowWarning-Konstruktor herum bewirkt, dass in Production nichts davon übrig bleibt. So bekommst du Debugging-Werkzeuge mit Null-Overhead-Garantie.

FAQ

Wann sollte ich Drop selbst implementieren?

Nur wenn dein Typ eine Ressource verwaltet, die explizit freigegeben werden muss: File-Handle, Socket, GPU-Buffer, externe Library-Pointer, FFI-Allocations. Für gewöhnliche Daten (Felder mit String, Vec) brauchst du Drop nicht — die haben schon eigene Drops.

drop()-Methode kann ich nicht direkt aufrufen.

Der Compiler verbietet v.drop(). Workaround: std::mem::drop(v) — das ist eine Funktion, die v konsumiert und dann den Compiler den Drop einsetzen lässt. Selten gebraucht — meistens reicht das automatische Drop am Scope-Ende.

Funktioniert Drop auch bei panic?

Ja, im Standard-Profil (panic = "unwind"). Der Stack wird abgewickelt, alle lokalen Werte gedroppt. Bei panic = "abort" (in Cargo.toml) wird der Prozess sofort beendet — kein Drop läuft. Wichtig für Code, der Cleanup garantieren muss.

Drop und Copy sind unverträglich.

Ein Typ kann nicht gleichzeitig Copy UND Drop sein. Logisch: Copy heißt „kostenlos kopierbar", Drop heißt „braucht Cleanup". Beides zusammen würde bedeuten, dass eine Kopie ein zusätzliches Cleanup brauchen würde — Double-Free. Der Compiler verbietet das.

Drop kann keinen Fehler zurückgeben.

Die Methode hat Signatur fn drop(&mut self) — keine Result-Rückgabe, kein ?. Wer Cleanup mit möglichem Fehler braucht (z. B. „Commit der Datei darf fehlschlagen"), gibt eine eigene close()/commit()-Methode mit Result-Rückgabe — und nutzt Drop als „Fallback, der best-effort versucht".

mem::forget ist safe Rust.

Memory-Leaks verletzen keine der drei Ownership-Regeln — sie sind nicht im Sinne von Memory-Safety unsafe. Daher ist mem::forget ohne unsafe-Block aufrufbar. Trotzdem: außer bei FFI selten gewollt.

Drop läuft AUCH bei std::process::exit?

Nein. std::process::exit(code) beendet den Prozess ohne Drop-Aufrufe. Wer auf Drops für kritisches Cleanup angewiesen ist, sollte exit vermeiden — stattdessen aus main mit Ok(()) zurückkehren und Drops normal laufen lassen.

Drop-Order in Tupeln und Arrays.

Tupel-Elemente: in Deklarations-Reihenfolge gedroppt (links zuerst). Array-Elemente: ebenfalls (Index 0 zuerst). Struct-Felder: Deklarations-Reihenfolge. Bei manuellen RAII-Wrappers mit Abhängigkeiten zwischen Feldern: Reihenfolge im Struct bewusst wählen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Ownership

Zur Übersicht