Einleitung: Warum Callbacks in Rust eine Herausforderung (und eine Chance) sind
\nWenn Sie jemals in JavaScript, Python oder C# geschrieben haben, sind Callbacks für Sie ein vertrautes Werkzeug. Sie übergeben eine Funktion, die später aufgerufen wird, und leben ruhig. Aber wenn Sie zu Rust kommen, verwandelt sich dieses einfache Muster plötzlich in ein Minenfeld: Ownership, Borrowing, Lifetimes, dyn vs impl...
Auf Stack Overflow hat die Frage "Idiomatic callbacks in Rust" Tausende von Aufrufen gesammelt. Das bedeutet, dass selbst erfahrene Entwickler über dieses Thema stolpern. Aber die gute Nachricht: Rust bietet nicht nur einen, sondern mehrere elegante Wege, Callbacks zu implementieren. Und heute werden wir sie alle durchgehen – von einfachen Closures bis hin zu leistungsstarken Trait-Objekten und Enum-Mustern.
\nDieser Artikel ist für Sie, wenn Sie:
\n- \n
- Angefangen haben, Rust zu lernen, und verstehen möchten, wie man asynchrone und ereignisgesteuerte Architekturen richtig organisiert; \n
- Von anderen Sprachen wechseln und nach idiomatischen Alternativen zu vertrauten Mustern suchen; \n
- Sauberen, sicheren und performanten Code in Rust schreiben möchten. \n
Los geht's!
\n\nDas Problem: Was ist falsch an einfachen Callbacks?
\nStellen Sie sich vor, wir schreiben einen einfachen Task-Scheduler. In Python würde das so aussehen:
\ndef schedule(callback):\n # mache etwas\n callback(result)\nIn Rust könnte der erste Versuch so sein:
\nfn schedule(callback: fn(i32) -> i32) {\n let result = 42;\n callback(result);\n}\nUnd das funktioniert! Aber nur für Funktionen, nicht für Closures. Versuchen Sie, ein Closure zu übergeben, das eine Variable erfasst:
\nlet factor = 2;\nlet closure = |x| x * factor;\nschedule(closure); // Fehler! expected fn pointer, found closure\nDer Compiler beschwert sich zu Recht: fn ist ein Funktionszeiger, aber ein Closure ist ein anonymer Typ, der die Umgebung erfassen kann. Wir brauchen einen flexibleren Ansatz.
Ansatz 1: Closures mit impl Fn (Einfachheit und Flexibilität)
\nDer einfachste und am häufigsten verwendete Weg ist die Verwendung eines generischen Typs mit dem Trait Fn, FnMut oder FnOnce.
Wann verwendet man Fn, FnMut, FnOnce?
\n- \n
FnOnce– wenn der Callback genau einmal aufgerufen wird (konsumiert erfasste Werte); \n FnMut– wenn der Callback den erfassten Zustand ändern kann (wird mehrmals aufgerufen); \n Fn– wenn der Callback den Zustand nicht ändert (reine Funktion). \n
Beispiel mit impl Fn:
fn schedule<F>(callback: F)\nwhere\n F: Fn(i32) -> i32,\n{\n let result = 42;\n println!("Ergebnis: {}", callback(result));\n}\n\nfn main() {\n let factor = 2;\n let closure = |x| x * factor;\n schedule(closure); // Funktioniert!\n \n // Man kann auch eine normale Funktion übergeben\n fn double(x: i32) -> i32 { x * 2 }\n schedule(double);\n}\nVorteile: maximal flexibel, unterstützt Closures, sicher, lesbar.
\nNachteile: Monomorphisierung – für jeden Closure-Typ generiert der Compiler eine separate Version der Funktion. Bei vielen Callbacks kann der Code aufgebläht werden.
Ansatz 2: Trait-Objekte Box<dyn Fn> (dynamische Dispatch)
\nWenn Sie Callbacks in Collections (z. B. in Vec) speichern oder zwischen Threads übergeben möchten, sind Generics nicht geeignet – es wird ein einziger Typ benötigt. Hier helfen Trait-Objekte:
type Callback = Box<dyn Fn(i32) -> i32>;\n\nstruct Scheduler {\n callbacks: Vec<Callback>,\n}\n\nimpl Scheduler {\n fn new() -> Self {\n Scheduler { callbacks: Vec::new() }\n }\n \n fn register(&mut self, callback