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):
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.
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: APIDie 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
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
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
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.
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
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:
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:
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: aDas 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:
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:
# 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.
# 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
unsafenö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.
# 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 nehmenDer 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
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
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
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
// 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
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)
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
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
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
- std::ops::Drop
- std::mem::drop
- std::mem::forget
- The Rust Book – Drop
- Rust Reference – Destructors
- The Rustonomicon – Drop Check