Zum Inhalt springen

Closures in Rust

Ein Closure (anonyme Funktion) ist eine kleine Funktion ohne Namen, die du in einer Variablen speichern und weitergeben kannst. Was Closures von normalen Funktionen unterscheidet: Sie können Variablen aus dem Gültigkeitsbereich (Scope), in dem sie entstanden sind, „einfangen“.

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

Ausgabe:

6
▶ Selbst ausprobieren

Die senkrechten Striche |x: i32| ersetzen die runden Klammern, die eine normale Funktion verwenden würde. Hier beide im direkten Vergleich:

fn add_one_fn(x: i32) -> i32 { x + 1 } // eine normale Funktion
let add_one_closure = |x: i32| x + 1; // dasselbe als Closure

Rust kann die Typen der Parameter und des Rückgabewerts oft selbst ableiten. Deshalb siehst du Closures häufig noch kürzer geschrieben:

let add_one = |x| x + 1;

Ein Closure ohne Parameter hat leere Striche: let say_hi = || println!("hi");. Aufrufen kannst du es mit say_hi().

Brauchst du mehr als einen Ausdruck, setzt du den Rumpf in geschweifte Klammern, genau wie bei einer Funktion:

fn main() {
let describe = |n: i32| {
let kind = if n % 2 == 0 { "gerade" } else { "ungerade" };
format!("{n} ist {kind}")
};
println!("{}", describe(7));
}

Ausgabe:

7 ist ungerade
▶ Selbst ausprobieren

format! funktioniert wie println!, gibt den Text aber nicht aus, sondern liefert ihn als String zurück. Hier ist dieser String der letzte Ausdruck in den geschweiften Klammern – also gibt das Closure ihn zurück.

Das kann eine normale Funktion nicht: Ein Closure darf Variablen von der Stelle benutzen, an der es definiert wurde – ohne dass du sie ausdrücklich übergibst:

fn main() {
let discount = 10;
let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90
println!("{}", apply_discount(50)); // 40
}

Ausgabe:

90
40
▶ Selbst ausprobieren

discount ist kein Parameter des Closures. Die Variable steht einfach im Scope von main, und das Closure greift direkt auf sie zu. Eine normale fn kann das nicht – ihr müsstest du discount als Argument übergeben.

Ein Closure kann eine eingefangene Variable auch ändern, solange diese Variable mut ist. Dabei gibt es eine zusätzliche Regel: Auch das Closure selbst muss in einer mut-Variablen stehen. Wenn du es aufrufst, ändert sich etwas – und Rust will, dass das in deinem Code sichtbar ist.

fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("count ist jetzt {count}");
};
increment();
increment();
increment();
println!("Endstand: {count}");
}

Ausgabe:

count ist jetzt 1
count ist jetzt 2
count ist jetzt 3
Endstand: 3
▶ Selbst ausprobieren

Vergisst du das mut bei increment, sagt dir der Compiler genau, was fehlt: calling increment requires mutable binding due to mutable borrow of count. Übersetzt: Der Aufruf von increment braucht eine veränderbare Bindung, weil das Closure count veränderbar ausleiht.

Einen Haken solltest du kennen. Solange das Closure noch aufgerufen werden kann, hat es das Recht, count zu ändern – und niemand sonst darf count anfassen. Schreibst du println!("{count}"); zwischen zwei increment()-Aufrufe, bekommst du error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable: count kann nicht unveränderbar ausgeliehen werden, weil es schon veränderbar ausgeliehen ist. Nach dem letzten Aufruf von increment() ist das Closure fertig, und count gehört wieder ganz dir. Deshalb funktioniert die Zeile mit dem Endstand. Die ganze Geschichte hinter dieser Regel erfährst du auf der Seite Borrowing.

Manchmal muss ein Closure das, was es einfängt, komplett übernehmen (Ownership) – zum Beispiel, wenn es an einen anderen Thread übergeben wird. Dafür schreibst du move vor die Striche: move || { ... }. Wie das in der Praxis aussieht, siehst du auf der Seite Nebenläufigkeit.

Für sich allein benutzt du Closures eher selten. Wirklich nützlich werden sie, wenn du einer anderen Funktion eigenes Verhalten mitgibst – vor allem den Iterator-Methoden, die du auf der Seite Iteratoren kennenlernst:

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

Ausgabe:

[2, 4, 6, 8, 10]
▶ Selbst ausprobieren

Hier ist |n| n * 2 ein Closure, das direkt an .map() übergeben wird. Genau so setzt du Closures im Alltag ein: als kleines Stück Code, das eine Methode wie .map() oder .filter() steuert.

Leg eine Variable tax_percent mit dem Wert 20 an. Schreib dann ein Closure with_tax, das einen Preis entgegennimmt und den Preis plus Steuer zurückgibt – mit dem eingefangenen tax_percent. Gib das Ergebnis für die Preise 50 und 120 aus. Tipp: Die Steuer ist price * tax_percent / 100.

Lösung anzeigen
fn main() {
let tax_percent = 20;
let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 mit Steuer sind {}", with_tax(50));
println!("120 mit Steuer sind {}", with_tax(120));
}

Ausgabe:

50 mit Steuer sind 60
120 mit Steuer sind 144
▶ Selbst ausprobieren

tax_percent wird nie übergeben. Das Closure fängt es aus main ein – genau das, was eine normale fn nicht könnte.

Schreib ein Closure next_id, das beim ersten Aufruf 1 zurückgibt, beim zweiten 2, beim dritten 3 und so weiter. Ruf es dreimal auf und gib jede ID aus. Tipp: Halte einen mut-Zähler außerhalb des Closures, erhöhe ihn darin um 1 und mach den Zähler zum letzten Ausdruck des Closures, damit er zurückgegeben wird.

Lösung anzeigen
fn main() {
let mut last_id = 0;
let mut next_id = || {
last_id += 1;
last_id
};
println!("erster Nutzer bekommt ID {}", next_id());
println!("zweiter Nutzer bekommt ID {}", next_id());
println!("dritter Nutzer bekommt ID {}", next_id());
}

Ausgabe:

erster Nutzer bekommt ID 1
zweiter Nutzer bekommt ID 2
dritter Nutzer bekommt ID 3
▶ Selbst ausprobieren

last_id und next_id brauchen beide mut: das erste, weil sich sein Wert ändert, das zweite, weil der Aufruf des Closures das Eingefangene verändert.

  • Ein Closure ist eine kleine anonyme Funktion: |parameter| ausdruck.
  • Die Typen werden oft abgeleitet, deshalb sind Closures meist kürzer als eine entsprechende fn.
  • Anders als eine normale Funktion kann ein Closure Variablen aus dem Scope einfangen, in dem es definiert wurde.
  • Ein Closure, das eine eingefangene Variable ändert, muss in einer mut-Variablen stehen. Es leiht sich diese Variable aus, solange es benutzt wird.
  • Am nützlichsten sind Closures, wenn du sie an andere Funktionen übergibst – vor allem an Iterator-Methoden wie .map().

Kurzer Check

1. Welche Zeichen verwendet ein Closure für die Parameter statt der runden Klammern einer Funktion?

2. Was kann ein Closure, was eine normale fn nicht kann?

3. Wo werden Closures laut dieser Seite in der Praxis am häufigsten verwendet?

4. Ein Closure führt count += 1 auf einer eingefangenen Variablen aus. Was brauchst du noch?

Punkte: 0 / 4