Rust-Closures
Eine Closure in Rust ist eine anonyme Funktion, die Variablen aus ihrer Umgebung erfassen und speichern kann.
Closures ermöglichen den Zugriff auf Variablen außerhalb des Gültigkeitsbereichs, in dem sie definiert sind, und können sie bei Bedarf in die Closure verschieben oder ausleihen.
Closures werden in Rust häufig in Bereichen wie funktionaler Programmierung, nebenläufiger Programmierung und ereignisgesteuerter Programmierung verwendet.
Unterschied zwischen Closures und Funktionen
Sowohl Closures als auch gewöhnliche Funktionen können ein Stück Logik kapseln, aber Closures haben eine zusätzliche Kernfähigkeit: das Erfassen von Variablen aus der externen Umgebung. Die folgende Tabelle listet die Hauptunterschiede zwischen beiden auf:
| Merkmal | Funktionsabschluss | Funktion |
|---|---|---|
| Anonymität | Kein Name; normalerweise einer Variablen zugewiesen | Hat einen festenfnname |
| Umgebungserfassung | Kann externe Variablen erfassen | Kann keine externen Variablen erfassen |
| Definitionssyntax | |parameters| expression | fn name(parameters) |
| Typinferenz | Parameter- und Rückgabetypen können normalerweise weggelassen werden | Parameter- und Rückgabetypen müssen explizit angegeben werden |
| Speicherung und Übergabe | Kann als Variable, Parameter oder Rückgabewert verwendet werden | Ebenfalls unterstützt |
Deklarieren und Aufrufen von Closures
Die grundlegende Syntax einer Closure lautet wie folgt:
let closure_name = |参数列表| 表达式或语句块;
Parameter können Typannotationen haben oder weggelassen werden — der Rust-Compiler leitet sie aus dem Kontext ab.
Beispiel
// Closure: Typannotationen weggelassen, Compiler leitet automatisch ab
let add_one = |x| x + 1;
println!("add_one(4) = {}", add_one(4)); // Ausgabe: 5
// Closure: Parameter- und Rückgabetypen explizit annotieren
let multiply = |a: i32, b: i32| -> i32 { a * b };
println!("multiply(3, 7) = {}", multiply(3, 7)); // Ausgabe: 21
// Mehrzeilige Closure erfordert geschweifte Klammern
let greet = |name: &str| {
let greeting = format!("Hello, {}!", name);
greeting
};
println!("{}", greet("example")); // Ausgabe: Hello, example!
}
Closures werden genau auf dieselbe Weise aufgerufen wie gewöhnliche Funktionen: Klammern nach dem Variablennamen anfügen und die Argumente übergeben.
Erfassen externer Variablen
Die wesentlichste Eigenschaft einer Closure ist ihre Fähigkeit, Variablen aus dem Gültigkeitsbereich zu erfassen, in dem sie definiert ist. Dies ist der grundlegende Unterschied zwischen Closures und gewöhnlichen Funktionen.
Closures können externe Variablen auf drei Arten erfassen:
| Erfassungsmethode | Entsprechende Rust-Semantik | Beschreibung |
|---|---|---|
| Erfassung per Referenz | Ähnlich wie&T | Standardverhalten; die Closure leiht sich die Variable aus, und sie bleibt außerhalb verwendbar |
| Erfassung per veränderlicher Ausleihe | Ähnlich wie&mut T | Die Closure muss die Variable ändern; die Closure selbst muss deklariert werden alsmut |
| Erfassung per Wert | Ähnlich wieT | Verwenden SiemoveSchlüsselwort, das die Eigentümerschaft der Variablen in die Closure verschiebt |
Für Typen, die
Copydas Trait (wie z. B.i32、bool),movenur eine Kopie des Werts erstellt wird und die externe Variable verwendbar bleibt. Wenn Sie also die Eigentümerschaftsübertragung demonstrieren, wählen SieString、Vecund andere Nicht-CopyTypen.
Erfassung per Referenz
Standardmäßig leiht sich eine Closure externe Variablen über eine unveränderliche Referenz aus. Nachdem die Ausleihe beendet ist, kann der äußere Gültigkeitsbereich die Variable weiterhin verwenden.
Beispiel
let text = String::from("example");
// Die Closure erfasst text per Referenz und übernimmt kein Eigentum
let print_text = || println!("text = {}", text);
print_text(); // Ausgabe: text = example
// Die Ausleihe ist beendet; text kann weiterhin außerhalb verwendet werden
println!("Still usable outside: {}", text);
}
Erfassung per veränderlicher Ausleihe
Wenn eine Closure eine erfasste Variable ändern muss, erfasst Rust sie per veränderlicher Referenz. In diesem Fall muss die Closure selbst deklariert werden alsmut。
Beispiel
let mut counter = 0;
// Die Closure erfasst counter per &mut; die Closure selbst muss ebenfalls als mut deklariert werden
let mut inc = || {
counter += 1;
};
inc();
inc();
println!("counter = {}", counter); // Ausgabe: counter = 2
}
Erfassung nach Wert (move)
Indem man dasmove-Schlüsselwort vor die Closure setzt, übernimmt die Closure das Eigentum an den erfassten Variablen. Nach der Übertragung des Eigentums kann der äußere Gültigkeitsbereich die Variable nicht mehr verwenden.
Beispiel
let owned = String::from("EXAMPLE");
// move überträgt das Eigentum von owned in die Closure
let take_owned = move || println!("owned = {}", owned);
take_owned(); // Ausgabe: owned = EXAMPLE
// Wenn Sie die Kommentarzeichen der folgenden Zeile entfernen, schlägt die Kompilierung fehl: Das Eigentum von owned wurde in die Closure verschoben.
// println!("{}", owned);
}
Wenn eine Closure an einen anderen Thread übergeben oder außerhalb ihres Gültigkeitsbereichs zurückgegeben werden muss,moveist dies besonders üblich — es stellt sicher, dass die Closure die erforderlichen Daten besitzt, und verhindert hängende Referenzen.
Closure-Traits: Fn / FnMut / FnOnce
Der Rust-Compiler implementiert automatisch das entsprechende Trait für eine Closure, basierend darauf, wie sie Variablen erfasst. Das Verständnis dieser drei Traits ist der Schlüssel zur Verwendung von Closures als Funktionsparameter oder Rückgabewerte.
| Trait | Erfassungsmethode | Anzahl der Aufrufe | Typisches Szenario |
|---|---|---|---|
Fn | Unveränderliche Ausleihe (&T) | Mehrmals | Nur-Lese-Zugriff auf erfasste Variablen |
FnMut | Veränderliche Ausleihe (&mut T) | Mehrmals | Muss erfasste Variablen ändern |
FnOnce | Übernimmt Eigentum (T) | Nur einmal | Verbraucht erfasste Variablen; kann danach nicht erneut aufgerufen werden |
Die Vererbungsbeziehung zwischen den dreien ist:FnJaFnMutist ein Subtrait von,FnMutJaFnOnceist ein Subtrait von. Das heißt, eine Closure, die implementiertFnimplementiert automatisch auchFnMutundFnOnce。
Hinweis:
moveDas Schlüsselwort erzwingt lediglich, dass Ownership übernommen wird; es bedeutet nicht, dass die Closure sein mussFnOnce. Wenn der Closure-Körper den erfassten Wert nicht konsumiert, selbst wennmovehinzugefügt wird, kann die Closure weiterhin implementierenFn(kann mehrmals aufgerufen werden).
Beispiel
let name = String::from("example");
// Fn-Closure: liest nur, ändert nichts und kann mehrmals aufgerufen werden
let greet = || println!("Hello, {}!", name);
greet(); // Erster Aufruf
greet(); // Zweiter Aufruf, funktioniert weiterhin einwandfrei
// FnMut-Closure: muss erfasste Variablen verändern
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("increment() = {}", increment()); // Ausgabe: 1
println!("increment() = {}", increment()); // Ausgabe: 2
// FnOnce-Closure: verbraucht erfasste Variablen, kann nur einmal aufgerufen werden
let data = String::from("BEISPIEL");
let consume = move || {
let _ = data; // Verschiebt data aus der Closure-Umgebung heraus und verbraucht sie
println!("data wurde verbraucht");
};
consume();
// consume(); // Wenn entkommentiert, schlägt die Kompilierung fehl: FnOnce-Closures können nur einmal aufgerufen werden
}
Closures als Parameter und Rückgabewerte
Closures können als Funktionsparameter übergeben und als Rückgabewerte von Funktionen zurückgegeben werden. Dies sind die beiden häufigsten Verwendungen von Closures in der realen Entwicklung.
Closures als Parameter
Wenn Sie eine Closure als Parameter verwenden, müssen Sie den Trait-Typ der Closure mit einer generischen Schranke angeben. Im folgenden Beispiel ist der ParameterFist beschränkt aufFn(i32) -> i32, was eine Closure bedeutet, die eineni32Parameter entgegennimmt undi32einen Wert zurückgibt.
Beispiel
fn apply<F>(val: i32, f: F) -> i32
wobei
F: Fn(i32) -> i32, // F muss Fn(i32) -> i32 implementieren
{
f(val)
}
fn main() {
let double = |x| x * 2;
let result = apply(5, double);
println!("Result: {}", result); // Ausgabe: Result: 10
// Du kannst auch einen anonymen Closure direkt übergeben
let result2 = apply(3, |x| x + 100);
println!("Result2: {}", result2); // Ausgabe: Result2: 103
}
Closures als Rückgabewerte
Da der Typ einer Closure anonym ist, erfordert das Zurückgeben einer Closureimpl TraitoderBox<dyn Trait>den Rückgabetyp zu beschreiben.
Zurückgeben einer Closure mit impl Fn
Wenn der Typ der zurückgegebenen Closure zur Kompilierzeit bestimmt werden kann, verwendeimpl Fnund keine Heap-Allokation ist nötig.
Beispiel
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// Muss move verwenden; andernfalls ist x eine lokale Referenz und x wird ungültig, nachdem die Closure zurückgegeben wurde
move |y| x + y
}
fn main() {
let add_five = make_adder(5);
println!("5 + 3 = {}", add_five(3)); // Ausgabe: 5 + 3 = 8
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // Ausgabe: 10 + 2 = 12
}
Verwende Box<dyn Fn>, um Closures zurückzugeben
Wenn du zur Laufzeit dynamisch verschiedene Closures zum Zurückgeben auswählen musst, verwendeBox<dyn Fn>Allokiere die Closure auf dem Heap.
Beispiel
Box::new(move |y| x + y)
}
fn main() {
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // Ausgabe: 10 + 2 = 12
}
| Methode | Allokationsort | Anwendbare Szenarien |
|---|---|---|
impl Fn | Stapel | Der Closure-Typ kann zur Kompilierungszeit bestimmt werden, was zu besserer Leistung führt |
Box<dyn Fn> | Heap | Closures zur Laufzeit dynamisch auswählen oder Closures über Funktionsgrenzen hinweg speichern müssen |
Häufige Anwendungsfälle
Closures sind in der täglichen Rust-Entwicklung allgegenwärtig. Hier sind einige der typischsten Anwendungsszenarien.
Closures in Iteratoren
Closures werden oft zusammen mit Iterator-Methoden verwendet, um Sammlungselemente stapelweise zu verarbeiten.
Beispiel
let nums = vec![1, 2, 3, 4, 5];
// map: jedes Element transformieren
let squared: Vec<i32> = nums.iter().map(|x| x * x).collect();
println!("squares: {:?}", squared); // Ausgabe: [1, 4, 9, 16, 25]
// filter: Elemente filtern, die die Bedingung erfüllen
let even: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
println!("evens: {:?}", even); // Ausgabe: [2, 4]
// fold: eine Sammlung auf einen einzelnen Wert reduzieren
let sum: i32 = nums.iter().fold(0, |acc, x| acc + x);
println!("sum: {}", sum); // Ausgabe: 15
}
Closures und Multithreading
In der Multithread-Programmierung werden Closures häufig verwendet, um den Ausführungskörper eines Threads zu definieren.moveDas `move`-Schlüsselwort ist in diesem Szenario nahezu unerlässlich, da es den Besitz der Daten in den neuen Thread verschiebt und verhindert, dass threadübergreifende Referenzen ungültig werden.
Beispiel
fn main() {
let nums = vec![1, 2, 3, 4, 5];
// Erstelle einen Thread für jede Zahl; move überträgt den Besitz von num in den Thread
let handles: Vec<_> = nums.into_iter().map(|num| {
thread::spawn(move || {
num * 2
})
}).collect();
// Warte, bis alle Threads abgeschlossen sind, und sammle die Ergebnisse
for handle in handles {
let result = handle.join().unwrap();
println!("Ergebnis: {}", result);
}
}
Closures und Fehlerbehandlung
Closures können zurückgebenResultoderOptionTypen, die in Kombination mit Iterator-Methoden eine prägnante Fehlerbehandlungslogik implementieren.
Beispiel
let nums = vec![3, -1, 4, -5, 9];
// Verwende eine Closure, um die erste positive Zahl zu finden
let first_positive = nums.iter().find(|&&x| x > 0);
match first_positive {
Some(&n) => println!("Erste positive Zahl: {}", n), // Ausgabe: Erste positive Zahl: 3
None => println!("Keine positiven Zahlen"),
}
// Verwende eine Closure zum Filtern und Transformieren, kombiniert mit Result, um mögliche Fehler zu behandeln
let results: Vec<Result<i32, &str>> = nums.iter().map(|&n| {
if n > 0 {
OK(n * 10)
} else {
Err("Negative Zahlen können nicht verarbeitet werden")
}
}).collect();
for r in results {
Treffer r {
Ok(v) => println!("Erfolg: {}", v),
Err(e) => println!("Fehler: {}", e),
}
}
}
Leistung und Lebensdauern
Leistung von Closures
Rusts Closures sind leichtgewichtig. Der Compiler führt Inlining-Optimierungen an Closures durch, wodurch der Overhead von Closure-Aufrufen nahe an dem direkten Aufruf gewöhnlicher Funktionen liegt. Closures selbst führen keine zusätzlichen virtuellen Funktionsaufrufe oder Heap-Allokationen ein (es sei denn, man verwendet explizitBox)。
Closures und Lebensdauern
Die Lebensdauer einer Closure ist eng mit den Variablen verbunden, die sie erfasst. Das Lebensdauersystem von Rust stellt sicher, dass eine Closure keine Variable überlebt, die sie erfasst – wenn eine Closure auf eine lokale Variable verweist, verhindert der Compiler zur Kompilierzeit, dass du die Closure aus dem Gültigkeitsbereich dieser Variable zurückgibst.
Beispiel
let text = String::from("example");
// Richtig: Verwende die Closure innerhalb des Gültigkeitsbereichs von text
let print_text = || println!("{}", text);
print_text(); // Ausgabe: Beispiel
// Wenn Sie versuchen, diese Closure zurückzugeben, meldet der Compiler einen Fehler:
// fn make_closure() -> impl Fn() {
// let text = String::from("example");
// || println!("{}", text) // Fehler: Die Lebensdauer von text ist nicht lang genug
// }
// Lösung: Verwenden Sie move, um die Eigentümerschaft in die Closure zu übertragen
}
Vollständiges Beispiel
Das folgende Beispiel zeigt umfassend die Kernverwendungen von Closures wie Deklaration, Erfassen externer Variablen und Übergabe als Argumente.
Beispiel
fn apply_operation<F>(num: i32, operation: F) -> i32
wobei
F: Fn(i32) -> i32,
{
operation(num)
}
fn main() {
let num = 5;
// Definiere eine Closure: eine Zahl quadrieren
let square = |x| x * x;
// Übergib die Closure als Argument an die Funktion
let result = apply_operation(num, square);
println!("Das Quadrat von {} ist {}", num, result); // Ausgabe: Das Quadrat von 5 ist 25
// Du kannst auch einen anonymen Closure direkt übergeben
let result2 = apply_operation(num, |x| x * x * x);
println!("Die dritte Potenz von {} ist {}", num, result2); // Ausgabe: Die dritte Potenz von 5 ist 125
}
Führe das Programm aus; die Ausgabe ist wie folgt:
Square of 5 is 25 Cube of 5 is 125
Zusammenfassung
Rusts Closures sind eine mächtige Abstraktion, die eine flexible und ausdrucksstarke Möglichkeit bietet, Logik zu kapseln.
Closures können Umgebungsvariablen erfassen und als Parameter übergeben oder als Rückgabewerte zurückgegeben werden. In Kombination mit Iteratoren können Closures bequem komplexe Datenverarbeitungsaufgaben umsetzen.
Rusts Closure-Design berücksichtigt Sicherheit, Leistung und Lebensdauer – der Compiler stellt zur Kompilierungszeit sicher, dass Closures keine ungültig gewordenen Variablen referenzieren, und garantiert durch Inlining-Optimierung Zero-Cost-Abstraktion für Closure-Aufrufe.
Weitere Erweiterungen