Funktion als Argument in Rust: Vom Anfänger zum Profi
\n\nHast du schon einmal in Python oder JavaScript geschrieben und mühelos eine Funktion an eine andere übergeben? Und dann bist du zu Rust gewechselt und bist auf der Stelle gestolpert? Kommt dir bekannt vor. Die Frage "How do you pass a Rust function as a parameter?" ist eine der beliebtesten auf Stack Overflow. Und das zu Recht: Das Typsystem und das Ownership-Modell von Rust fügen mehrere Komplexitätsebenen hinzu, eröffnen aber auch neue Möglichkeiten.
\n\nIn diesem Artikel behandeln wir drei grundlegende Ansätze: Funktionszeiger (fn), Closures und Traits. Du erfährst, wann du welche Methode verwendest und verlierst die Angst vor dem funktionalen Stil in Rust.
Warum ist das wichtig?
\nDie Fähigkeit, Funktionen als Parameter zu übergeben, ist kein akademischer Trick. Sie ist die Grundlage für:
\n- \n
- Die Erstellung flexibler und wiederverwendbarer Bibliotheken (z. B. Iteratoren, Event-Handler); \n
- Die Implementierung der Entwurfsmuster "Strategie" und "Beobachter"; \n
- Das Schreiben von prägnantem und ausdrucksstarkem Code ohne viele if-else-Anweisungen. \n
In Rust, im Gegensatz zu dynamischen Sprachen, prüft der Compiler streng Typen und Lebenszeiten. Daher musst du genau verstehen, was du übergibst: eine einfache Funktionsadresse, eine Closure mit Kontexterfassung oder etwas Komplexeres.
\n\nAnsatz 1: Funktionszeiger (Function Pointer) — fn
\nDer einfachste und direkteste Weg. Wenn deine Funktion keine Variablen aus der Umgebung erfasst (sie ist "rein"), kannst du den Typ fn verwenden. Achtung: fn ist ein Typ, während Fn, FnMut und FnOnce Traits sind. Verwechsle sie nicht!
fn add_one(x: i32) -> i32 {\n x + 1\n}\n\n// Funktion, die eine andere Funktion entgegennimmt\nfn apply_twice(f: fn(i32) -> i32, value: i32) -> i32 {\n f(f(value))\n}\n\nfn main() {\n let result = apply_twice(add_one, 5);\n println!("Ergebnis: {}", result); // 7\n}\n\nWann verwenden? Wenn die Funktion zur Kompilierzeit bekannt ist, keine Variablen erfasst und du nicht die Leistung von Closures benötigst. Funktionszeiger sind eine Zero-Cost-Abstraktion, sie erfordern keine Speicherallokationen.
\n\nEinschränkungen von fn
\nDer größte Nachteil: Du kannst keine Closure übergeben, die Variablen erfasst. Der folgende Code wird nicht kompilieren:
\n\nfn main() {\n let y = 10;\n // Closure erfasst y\n let closure = |x: i32| -> i32 { x + y };\n // Fehler! expected fn item, found closure\n // let result = apply_twice(closure, 5);\n}\n\nDafür benötigst du die Traits Fn, FnMut und FnOnce.
Ansatz 2: Closures und die Traits Fn, FnMut, FnOnce
\nRust ist eine systemnahe Sprache und muss wissen, was mit dem Speicher passiert. Closures können Variablen auf drei Arten erfassen:
\n- \n
- Per Referenz (&T) — der Trait
Fn. Kann beliebig oft aufgerufen werden, ohne die erfassten Daten zu ändern. \n - Per veränderlicher Referenz (&mut T) — der Trait
FnMut. Kann mehrmals aufgerufen werden, aber die Daten können sich ändern. \n - Per Wert (T) — der Trait
FnOnce. Kann nur einmal aufgerufen werden, danach wird die Closure "konsumiert". \n
Beispiel mit Fn
\nfn apply_twice_fn<F>(f: F, value: i32) -> i32\nwhere\n F: Fn(i32) -> i32,\n{\n f(f(value))\n}\n\nfn main() {\n let y = 10;\n let closure = |x| x + y; // erfasst y per Referenz\n let result = apply_twice_fn(closure, 5);\n println!("Fn: {}", result); // 25\n}\n\nBeispiel mit FnMut
\nfn apply_twice_mut<F>(mut f: F, value: i32) -> i32\nwhere\n F: FnMut(i32) -> i32,\n{\n f(f(value))\n}\n\nfn main() {\n let mut counter = 0;\n let mut closure = |x: i32| -> i32 {\n counter += 1;\n x + counter\n };\n let result = apply_twice_mut(closure, 5);\n println!("FnMut: {}", result); // 8 (5+1 + 6+2)\n}\n\nBeispiel mit FnOnce
\nfn consume<F>(f: F, value: i32) -> i32\nwhere\n F: FnOnce(i32) -> i32,\n{\n f(value) // kann nur einmal aufgerufen werden\n}\n\nfn main() {\n let s = String::from("Hallo");\n let closure = |x: i32| -> i32 {\n println!("{}", s); // s wird konsumiert\n x + 1\n };\n let result = consume(closure, 5);\n println!("FnOnce: {}", result); // 6\n // closure kann hier nicht mehr verwendet werden\n}\n\nAnsatz 3: Traits für maximale Flexibilität
\nWenn du noch mehr Kontrolle benötigst, kannst du eigene Traits definieren. Das ist besonders nützlich, wenn du mehrere zusammengehörige Funktionen übergeben oder den Zustand verwalten möchtest.
\n\ntrait Transformer {\n fn transform(&self, x: i32) -> i32;\n}\n\nstruct AddOne;\nimpl Transformer for AddOne {\n fn transform(&self, x: i32) -> i32 {\n x + 1\n }\n}\n\nstruct MultiplyBy {\n factor: i32,\n}\nimpl Transformer for MultiplyBy {\n fn transform(&self, x: i32) -> i32 {\n x * self.factor\n }\n}\n\nfn apply_twice_trait(t: &dyn Transformer, value: i32) -> i32 {\n t.transform(t.transform(value))\n}\n\nfn main() {\n let add_one = AddOne;\n let multiply = MultiplyBy { factor: 3 };\n println!("Trait AddOne: {}", apply_twice_trait(&add_one, 5)); // 7\n println!("Trait Multiply: {}", apply_twice_trait(&multiply, 5)); // 45\n}\n\nWann welchen Ansatz wählen?
\n- \n
fn(Funktionszeiger): Für einfache, reine Funktionen ohne Kontexterfassung. Am schnellsten und speichereffizientesten. \n Fn/FnMut/FnOnce(Closures): Für die meisten Fälle, in denen du Kontext benötigst. Flexibel und typsicher. \n - Eigene Traits: Für komplexe Szenarien mit mehreren Methoden oder Zustandsverwaltung. \n
Fazit
\nDas Übergeben von Funktionen als Parameter in Rust ist mächtig, erfordert aber ein gutes Verständnis der Typen und Traits. Mit den drei Ansätzen – Funktionszeigern, Closures und eigenen Traits – bist du für alle Situationen gerüstet. Übe mit den Beispielen und integriere sie in deine Projekte. Du wirst sehen, wie elegant und sicher funktionaler Code in Rust sein kann!