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“.
Die Grundsyntax
Abschnitt betitelt „Die Grundsyntax“fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}Ausgabe:
6Die 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 Funktionlet add_one_closure = |x: i32| x + 1; // dasselbe als ClosureRust 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().
Closures über mehrere Zeilen
Abschnitt betitelt „Closures über mehrere Zeilen“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 ungeradeformat! 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.
Variablen aus der Umgebung einfangen
Abschnitt betitelt „Variablen aus der Umgebung einfangen“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:
9040discount 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.
Closures, die eingefangene Variablen ändern
Abschnitt betitelt „Closures, die eingefangene Variablen ändern“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 1count ist jetzt 2count ist jetzt 3Endstand: 3Vergisst 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.
Wo Closures in der Praxis auftauchen
Abschnitt betitelt „Wo Closures in der Praxis auftauchen“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]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.
Übungen
Abschnitt betitelt „Übungen“1. Preise mit Steuer
Abschnitt betitelt „1. Preise mit Steuer“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 60120 mit Steuer sind 144tax_percent wird nie übergeben. Das Closure fängt es aus main ein – genau das, was eine normale fn nicht könnte.
2. Ein ID-Generator
Abschnitt betitelt „2. Ein ID-Generator“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 1zweiter Nutzer bekommt ID 2dritter Nutzer bekommt ID 3last_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.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- 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
Abschnitt betitelt „Kurzer Check“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
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Closures – Das Rust-Buch: das offizielle Kapitel über Closures, Typableitung und die
Fn-Traits. - Erfassen von Referenzen oder Verschieben des Eigentums – Das Rust-Buch: die drei Arten, wie ein Closure eine Variable einfangen kann, und wann du
movebrauchst. - Closures – Rust by Example (auf Englisch): kurze, ausführbare Beispiele, die Closures mit Funktionen vergleichen.
- Capturing – Rust by Example (auf Englisch): weitere Beispiele für Closures, die Variablen lesen, ändern und übernehmen.
- Closure expressions – The Rust Reference (auf Englisch): die genauen Regeln für die Closure-Syntax und das Einfangen.