Zum Inhalt springen

Fn, FnMut und FnOnce in Rust

Auf der Seite Closures hast du gelernt, wie du ein Closure (anonyme Funktion) schreibst, wie es Variablen einfängt und wie es sie verändern kann. Diese Seite beantwortet die nächste Frage: Wie schreibst du eine eigene Funktion, die ein Closure annimmt? Und wie gibst du ein Closure aus einer Funktion zurück?

Die Antwort sind drei kleine Traits (Eigenschaften, die ein Typ erfüllt): Fn, FnMut und FnOnce. Stell sie dir wie die Regeln in einer Bibliothek vor. Manche Besucher lesen die Bücher nur (Fn). Manche kritzeln Notizen an den Rand (FnMut). Und manche nehmen ein Buch mit nach Hause und bringen es nie zurück (FnOnce). Rust schaut sich an, was dein Closure tut, und gibt ihm das passende Etikett. Falls die Traits am Anfang einschüchternd wirken: keine Sorge. Es sind nur drei, und der Compiler wählt den richtigen für dich aus.

Jedes Closure hat seinen eigenen, einzigartigen Typ, den sich der Compiler ausdenkt. Den Namen dieses Typs kannst du nicht hinschreiben. Selbst zwei Closures, die genau gleich aussehen, haben verschiedene Typen:

fn main() {
let mut rules = vec![|price: u32| price * 2];
rules.push(|price: u32| price * 2);
}
error[E0308]: mismatched types
--> src/main.rs:3:16
|
3 | rules.push(|price: u32| price * 2);
| ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
|
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object

Der Compiler sagt: „erwartet wurde ein Closure, gefunden ein anderes Closure“ – keine zwei Closures haben denselben Typ, auch wenn sie identisch sind.

Eine Funktion kann also nicht sagen: „Gib mir ein Closure vom Typ X.“ Stattdessen sagt sie: „Gib mir irgendeinen Typ, den man so aufrufen kann.“ Das ist ein Trait Bound, genau wie auf den Seiten Generics und Traits:

fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 {
rule(price)
}
fn main() {
let coupon = 15;
let half_price = |price: u32| price / 2;
let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80));
println!("{}", price_after(use_coupon, 80));
}

Ausgabe:

40
65
▶ Selbst ausprobieren

Lies F: Fn(u32) -> u32 so: „F ist irgendein Typ, den du mit einem u32 aufrufen kannst, und er gibt einen u32 zurück.“ Beide Closures passen, obwohl ihre Typen verschieden sind.

Der Compiler schaut sich an, was der Rumpf des Closures mit den eingefangenen Werten macht:

  • Fn: Es liest sie nur. Du kannst es so oft aufrufen, wie du willst.
  • FnMut: Es verändert sie. Du kannst es oft aufrufen, aber es braucht mut.
  • FnOnce: Es gibt sie weg (verschiebt sie nach draußen). Du kannst es nur einmal aufrufen, denn danach ist der Wert weg.
fn main() {
let shop = String::from("Bäckerei Sonnenschein");
// Fn: liest `shop` nur
let greet = || println!("Willkommen in der {shop}!");
greet();
greet();
// FnMut: verändert `orders`
let mut orders = 0;
let mut take_order = || {
orders += 1;
println!("Bestellung Nr. {orders} aufgenommen");
};
take_order();
take_order();
// FnOnce: gibt `receipt` an den Aufrufer zurück
let receipt = String::from("2 Croissants, 1 Kaffee");
let hand_over = || receipt;
let customer_copy = hand_over();
println!("Der Kunde hat: {customer_copy}");
}

Ausgabe:

Willkommen in der Bäckerei Sonnenschein!
Willkommen in der Bäckerei Sonnenschein!
Bestellung Nr. 1 aufgenommen
Bestellung Nr. 2 aufgenommen
Der Kunde hat: 2 Croissants, 1 Kaffee
▶ Selbst ausprobieren

hand_over gibt receipt selbst zurück, keine Kopie. Sobald der Kassenbon übergeben ist, hat das Closure nichts mehr zu geben. Deshalb ist es FnOnce.

Dieses Bild solltest du im Kopf behalten. Stell dir drei Kreise vor, einer im anderen:

  • Der große äußere Kreis ist FnOnce. Jedes Closure lässt sich mindestens einmal aufrufen, also liegt jedes Closure hier drin.
  • Darin liegt ein kleinerer Kreis, FnMut: Closures, die du immer wieder aufrufen kannst.
  • Ganz in der Mitte liegt Fn: Closures, die du immer wieder aufrufen kannst, ohne dass sich etwas ändert.

Jedes Fn-Closure ist also auch ein FnMut und ein FnOnce. Das ist wichtig, wenn du eine Funktion schreibst. Ein Parameter, der FnOnce verlangt, nimmt jedes Closure an. Ein Parameter, der Fn verlangt, ist am wählerischsten.

Eine Sache überrascht viele: Das Schlüsselwort move bestimmt den Trait nicht. move || println!("{shop}") übernimmt das Ownership (Eigentum) an shop, liest es aber nur. Also ist es immer noch Fn. Allein der Rumpf entscheidet.

Es gibt drei Schreibweisen für denselben Bound. Sie bedeuten alle dasselbe, also nimm die, die sich am besten liest:

// 1. Ein generischer Parameter mit Bound
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 2. `impl Fn` als Parametertyp: kürzer, kein Name nötig
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// 3. Eine `where`-Klausel: übersichtlich, wenn der Bound lang wird
fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
scores.iter().map(|&s| f(s)).collect()
}
fn main() {
let bonus = 10;
let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50));
println!("{}", apply_twice(add_bonus, 50));
println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));
}

Ausgabe:

60
70
[50, 75, 100]
▶ Selbst ausprobieren

Vielleicht fragst du dich, wieso wir add_bonus dreimal übergeben konnten. Ein Closure, das seine eingefangenen Werte nur liest, lässt sich billig kopieren. Also kopiert Rust es jedes Mal.

Jetzt zu FnMut und FnOnce. Bei FnMut braucht der Parameter ein mut, weil der Aufruf das Innere des Closures verändert. Ein FnOnce kannst du einmal aufrufen, und das war’s:

fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
for task in tasks {
on_done(task);
}
}
fn on_shutdown<F: FnOnce()>(cleanup: F) {
println!("fahre herunter...");
cleanup();
}
fn main() {
let mut finished = Vec::new();
process_tasks(&["E-Mails senden", "Datenbank sichern"], |task| {
finished.push(task.to_string());
});
println!("erledigt: {:?}", finished);
on_shutdown(|| {
// Diese Schleife übernimmt `finished`, also ist das Closure FnOnce
for task in finished {
println!("archiviert: {task}");
}
});
}

Ausgabe:

erledigt: ["E-Mails senden", "Datenbank sichern"]
fahre herunter...
archiviert: E-Mails senden
archiviert: Datenbank sichern
▶ Selbst ausprobieren

Eine gute Faustregel: Verlange den lockersten Trait, mit dem deine Funktion auskommt. Rufst du das Closure nur einmal auf, verlange FnOnce. Dann können Aufrufer alles übergeben. Das siehst du auch in der Standardbibliothek: Option::map nimmt ein FnOnce, Iterator::map dagegen ein FnMut.

Der Compiler findet jede Unstimmigkeit, und seine Meldungen sind hier sehr hilfreich. Angenommen, eine Funktion verlangt Fn, aber dein Closure erhöht einen Zähler:

fn run_report<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let mut runs = 0;
run_report(|| {
runs += 1;
println!("Bericht Nr. {runs}");
});
}
error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure
--> src/main.rs:9:9
|
1 | fn run_report<F: Fn()>(f: F) {
| - change this to accept `FnMut` instead of `Fn`
...
8 | run_report(|| {
| ---------- -- in this closure
| |
| expects `Fn` instead of `FnMut`
9 | runs += 1;
| ^^^^^^^^^ cannot assign

Die Meldung heißt: „runs kann nichts zugewiesen werden, weil es eine eingefangene Variable in einem Fn-Closure ist.“ Sie verrät dir sogar die Lösung: Nimm FnMut an und setz mut vor den Parameter. Mit dieser Änderung gibt das Programm Bericht Nr. 1 und Bericht Nr. 2 aus:

fn run_report<F: FnMut()>(mut f: F) {
f();
f();
}

Der andere Klassiker ist, ein FnOnce-Closure zweimal aufzurufen:

fn main() {
let report = String::from("Umsatz Q3");
let send = || {
let sent = report;
println!("sende {sent}");
};
send();
send();
}
error[E0382]: use of moved value: `send`
--> src/main.rs:8:5
|
7 | send();
| ------ `send` moved due to this call
8 | send();
| ^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `report` out of its environment
--> src/main.rs:4:20
|
4 | let sent = report;
| ^^^^^^

Der Hinweis (note) sagt es klar: Das Closure kann nicht mehr als einmal aufgerufen werden, weil es report aus seiner Umgebung herausverschiebt. Der erste Aufruf hat report weggegeben, für einen zweiten bleibt nichts übrig. Brauchst du wirklich zwei Aufrufe, dann leih dir den Wert stattdessen aus (let sent = &report;), und das Closure wird zu Fn. Übergibst du so ein Closure an eine Funktion, die Fn verlangt, bekommst du eine ähnliche Meldung: error[E0507]: cannot move out of report, a captured variable in an Fn closure.

Eine Funktion kann ein Closure auch bauen und zurückgeben. Den Rückgabetyp schreibst du als impl Fn(i32) -> i32. Das bedeutet: „irgendein Closure-Typ; seinen Namen musst du nicht kennen“:

fn make_adder(amount: i32) -> impl Fn(i32) -> i32 {
move |x| x + amount
}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let add_tip = make_adder(5);
let triple = make_multiplier(3);
println!("{}", add_tip(20));
println!("{}", triple(7));
println!("{}", add_tip(triple(10)));
}

Ausgabe:

25
21
35
▶ Selbst ausprobieren

Warum das move? Ohne move würde sich das Closure amount nur ausleihen. Aber amount gehört make_adder und verschwindet in dem Moment, in dem make_adder zurückkehrt. Das Closure hielte dann eine Referenz auf nichts. Rust weigert sich:

error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |x| x + amount
| ^^^ ------ `amount` is borrowed here
| |
| may outlive borrowed value `amount`
|
help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x + amount
| ++++

Der Compiler warnt: Das Closure könnte länger leben als die Funktion, leiht sich aber amount aus, das der Funktion gehört. Und er schlägt gleich move vor. Mit move packt das Closure seine eigene Kopie von amount ein und trägt sie mit sich – so, als würdest du dein Pausenbrot mitnehmen, statt es im Kühlschrank von jemand anderem zu lassen.

impl Fn steht für einen versteckten Typ. Du kannst also nicht im if-Zweig ein Closure zurückgeben und im else-Zweig ein anderes. Du kannst auch keine verschiedenen Closures in denselben Vec legen. Für beides steckst du jedes Closure in eine Box: Box<dyn Fn(f64) -> f64> bedeutet „irgendein Closure mit dieser Form, auf dem Heap gespeichert“. Das ist dieselbe Idee wie auf der Seite Trait-Objekte.

Hier ist eine Kasse, die eine Liste benannter Preisregeln der Reihe nach anwendet:

fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
if express {
Box::new(move |total| total + fee * 2.0)
} else {
Box::new(move |total| total + fee)
}
}
fn main() {
let member_percent = 10.0;
let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![
("Mitgliederrabatt", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))),
("Gutschein", Box::new(move |total: f64| total - coupon)),
("Standardversand", shipping_rule(false, 3.5)),
];
let mut total = 80.0;
println!("Start: {total:.2}");
for (name, rule) in &rules {
total = rule(total);
println!("nach {name}: {total:.2}");
}
}

Ausgabe:

Start: 80.00
nach Mitgliederrabatt: 72.00
nach Gutschein: 67.00
nach Standardversand: 70.50
▶ Selbst ausprobieren

Die Box kostet eine winzige Speicherreservierung auf dem Heap, und jeder Aufruf läuft über einen Zeiger. Das ist fast nie ein Problem. Nimm impl Fn, wenn es einen einzigen Closure-Typ gibt, und Box<dyn Fn>, wenn es viele sind.

Auch ein Struct kann ein Closure enthalten. Da du den Typ des Closures nicht benennen kannst, machst du das Struct darüber generisch. Hier ist ein kleiner Button, der beim Klicken Code ausführt:

struct Button<F: Fn()> {
label: String,
on_click: F,
}
impl<F: Fn()> Button<F> {
fn click(&self) {
println!("[{}] geklickt", self.label);
(self.on_click)();
}
}
fn main() {
let user = String::from("Ada");
let save = Button {
label: String::from("Speichern"),
on_click: move || println!("speichere den Entwurf von {user}"),
};
save.click();
save.click();
}

Ausgabe:

[Speichern] geklickt
speichere den Entwurf von Ada
[Speichern] geklickt
speichere den Entwurf von Ada
▶ Selbst ausprobieren

Achte auf die Klammern in (self.on_click)(). Sie sagen Rust: „Nimm das Feld und ruf es dann auf.“ Soll der Button bei jedem Klick seine eingefangenen Werte verändern, nimm F: FnMut() und lass die Methode &mut self annehmen.

Auch normale Funktionen kannst du weitergeben. Ihr Typ wird mit einem kleinen fn geschrieben, etwa fn(i32) -> i32. Das ist ein Funktionszeiger (Function Pointer), also einfach die Adresse von etwas Code. Ein Closure, das nichts einfängt, kann sich ebenfalls in einen verwandeln:

fn double(x: i32) -> i32 {
x * 2
}
fn square(x: i32) -> i32 {
x * x
}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 {
op(x)
}
fn main() {
println!("{}", apply_op(double, 4));
println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1];
let mut value = 3;
for step in steps {
value = step(value);
}
println!("{value}");
}

Ausgabe:

8
3
37
▶ Selbst ausprobieren

Alle Funktionszeiger mit derselben Signatur teilen sich einen Typ. Deshalb kannst du sie ohne Box in ein Array legen. Der Haken: Ein Closure, das etwas einfängt, kann kein Funktionszeiger werden. Der Compiler sagt dann closures can only be coerced to fn types if they do not capture any variables – Closures lassen sich nur dann in fn-Typen umwandeln, wenn sie keine Variablen einfangen. Die gute Nachricht gilt in die andere Richtung: Jede normale Funktion implementiert auch Fn, FnMut und FnOnce. Du kannst double also an das apply von oben übergeben. Deshalb verwendet der meiste Code die Closure-Traits: Sie akzeptieren beides.

mut beim FnMut-Parameter vergessen. Ein Aufruf verändert ein FnMut, deshalb muss seine Variable mut sein:

error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable
--> src/main.rs:3:9
|
3 | on_done(task);
| ^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
| +++

Übersetzt: on_done kann nicht veränderbar ausgeliehen werden, weil es nicht als veränderbar deklariert ist. Der Vorschlag darunter zeigt dir genau, wo das mut hingehört.

Verschiedene Closures aus -> impl Fn zurückgeben. Zwei Closures sind zwei Typen, auch wenn sie gleich aussehen:

fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
if express {
move |total| total + fee * 2.0
} else {
move |total| total + fee
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if express {
3 | | move |total| total + fee * 2.0
| | ------------------------------
| | |
| | the expected closure
| | expected because of this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected closure `{closure@src/main.rs:3:9: 3:21}`
found closure `{closure@src/main.rs:5:9: 5:21}`
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object
help: you could change the return type to be a boxed trait object
|
1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new(move |total| total + fee * 2.0)
4 | } else {
5 ~ Box::new(move |total| total + fee)
|

if und else haben hier unvereinbare Typen. Mach, was der Compiler vorschlägt: Gib Box<dyn Fn(f64) -> f64> zurück und verpack jedes Closure in Box::new(...).

Ein Closure-Feld wie eine Methode aufrufen. Wenn du self.on_click() schreibst, sucht Rust nach einer Methode namens on_click:

error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope
--> src/main.rs:9:14
|
9 | self.on_click();
| ^^^^^^^^ field, not a method
|
help: to call the type parameter stored in `on_click`, surround the field access with parentheses
|
9 | (self.on_click)();
| + +

Die Meldung sagt: Das ist ein Feld, keine Methode. Setz den Feldzugriff in Klammern, dann klappt der Aufruf.

Schreib eine Funktion ticket_dispenser(start: u32), die ein Closure zurückgibt. Jeder Aufruf des Closures liefert die nächste Ticketnummer: erst start, dann start + 1 und so weiter. Bau zwei Automaten (eine Bäckerei, die bei 100 beginnt, und eine Apotheke, die bei 1 beginnt) und zeig, dass sie getrennt zählen. Tipp: Das Closure verändert seinen eigenen Zähler. Der Rückgabetyp ist also impl FnMut() -> u32, und du brauchst move.

Lösung anzeigen
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 {
let mut next = start;
move || {
let ticket = next;
next += 1;
ticket
}
}
fn main() {
let mut bakery = ticket_dispenser(100);
let mut pharmacy = ticket_dispenser(1);
println!("Bäckerei: {}", bakery());
println!("Bäckerei: {}", bakery());
println!("Apotheke: {}", pharmacy());
println!("Bäckerei: {}", bakery());
}

Ausgabe:

Bäckerei: 100
Bäckerei: 101
Apotheke: 1
Bäckerei: 102
▶ Selbst ausprobieren

Jeder Automat hat sein eigenes next per move eingepackt, deshalb teilen sie sich nie einen Zähler. Die Variablen bakery und pharmacy brauchen mut, weil sie FnMut sind.

Schreib retry(max_attempts, task). Die Funktion ruft task so lange auf, bis es Ok zurückgibt oder bis sie es max_attempts-mal versucht hat. Gib für jeden fehlgeschlagenen Versuch eine Zeile aus. Teste sie mit einem Closure, das zweimal mit "Server ausgelastet" scheitert und dann klappt. Tipp: Das Test-Closure zählt seine eigenen Aufrufe. Der Bound ist also FnMut() -> Result<String, String>, und der Parameter braucht mut.

Lösung anzeigen
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>
where
F: FnMut() -> Result<String, String>,
{
for attempt in 1..=max_attempts {
match task() {
Ok(value) => return Ok(value),
Err(e) => println!("Versuch {attempt} fehlgeschlagen: {e}"),
}
}
Err(String::from("aufgegeben"))
}
fn main() {
let mut calls = 0;
let result = retry(5, || {
calls += 1;
if calls < 3 {
Err(String::from("Server ausgelastet"))
} else {
Ok(String::from("Daten heruntergeladen"))
}
});
println!("{:?} nach {calls} Aufrufen", result);
}

Ausgabe:

Versuch 1 fehlgeschlagen: Server ausgelastet
Versuch 2 fehlgeschlagen: Server ausgelastet
Ok("Daten heruntergeladen") nach 3 Aufrufen
▶ Selbst ausprobieren

retry ruft die Aufgabe mehr als einmal auf, also reicht FnOnce nicht. Und die Aufgabe verändert calls, also reicht auch Fn nicht. FnMut passt genau. Sobald retry zurückkehrt, ist das Closure fertig, und main kann calls wieder lesen.

  • Jedes Closure hat seinen eigenen, einzigartigen Typ. Deshalb nehmen Funktionen Closures über einen Trait Bound wie F: Fn(i32) -> i32 an.
  • Fn liest seine eingefangenen Werte nur, FnMut verändert sie, und FnOnce verschiebt sie nach draußen und lässt sich nur einmal aufrufen. Der Rumpf entscheidet, nicht move.
  • Jedes Fn ist auch FnMut und FnOnce. Verlange den lockersten Trait, den deine Funktion braucht.
  • Schreib Bounds als <F: Fn(..)>, als impl Fn(..) oder in einer where-Klausel. FnMut-Parameter brauchen mut.
  • Gib ein einzelnes Closure mit -> impl Fn(..) zurück (meist mit move). Für verschiedene Closures aus mehreren Zweigen oder in einem Vec nimm Box<dyn Fn(..)>.
  • fn(i32) -> i32 ist ein Funktionszeiger. Normale Funktionen und Closures, die nichts einfangen, passen hinein.

Kurzer Check

1. Ein Closure führt total += price auf einer eingefangenen Variablen aus. Welches ist der strengste Trait, den es implementiert?

2. Deine Funktion ruft ihren Closure-Parameter genau einmal auf. Welcher Bound akzeptiert die meisten Closures?

3. Warum braucht make_adder(amount: i32) -> impl Fn(i32) -> i32 ein move |x| x + amount?

4. Du willst einen Vec mit mehreren verschiedenen Rabatt-Closures. Welchen Typ brauchen die Elemente?

Punkte: 0 / 4