Funktionen als Argumente in Rust: 3 Wege zur Übergabe

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Funktion als Argument in Rust: Vom Anfänger zum Profi

\n\n

Hast 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\n

In 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.

\n\n

Warum ist das wichtig?

\n

Die 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
\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\n

Ansatz 1: Funktionszeiger (Function Pointer) — fn

\n

Der 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!

\n\n
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\n

Wann 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\n

Einschränkungen von fn

\n

Der größte Nachteil: Du kannst keine Closure übergeben, die Variablen erfasst. Der folgende Code wird nicht kompilieren:

\n\n
fn 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\n

Dafür benötigst du die Traits Fn, FnMut und FnOnce.

\n\n

Ansatz 2: Closures und die Traits Fn, FnMut, FnOnce

\n

Rust 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
\n\n

Beispiel mit Fn

\n
fn 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\n

Beispiel mit FnMut

\n
fn 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\n

Beispiel mit FnOnce

\n
fn 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\n

Ansatz 3: Traits für maximale Flexibilität

\n

Wenn 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\n
trait 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\n

Wann 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
\n\n

Fazit

\n

Das Ü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!

Blogs

Buchempfehlungen