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:

MerkmalFunktionsabschlussFunktion
AnonymitätKein Name; normalerweise einer Variablen zugewiesenHat einen festenfnname
UmgebungserfassungKann externe Variablen erfassenKann keine externen Variablen erfassen
Definitionssyntax|parameters| expressionfn name(parameters)
TypinferenzParameter- und Rückgabetypen können normalerweise weggelassen werdenParameter- und Rückgabetypen müssen explizit angegeben werden
Speicherung und ÜbergabeKann als Variable, Parameter oder Rückgabewert verwendet werdenEbenfalls 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

fn main() {
    // 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:

ErfassungsmethodeEntsprechende Rust-SemantikBeschreibung
Erfassung per ReferenzÄhnlich wie&TStandardverhalten; die Closure leiht sich die Variable aus, und sie bleibt außerhalb verwendbar
Erfassung per veränderlicher AusleiheÄhnlich wie&mut TDie Closure muss die Variable ändern; die Closure selbst muss deklariert werden alsmut
Erfassung per WertÄhnlich wieTVerwenden SiemoveSchlüsselwort, das die Eigentümerschaft der Variablen in die Closure verschiebt

Für Typen, dieCopydas 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

fn main() {
    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

fn main() {
    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

fn main() {
    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.

TraitErfassungsmethodeAnzahl der AufrufeTypisches Szenario
FnUnveränderliche Ausleihe (&T)MehrmalsNur-Lese-Zugriff auf erfasste Variablen
FnMutVeränderliche Ausleihe (&mut T)MehrmalsMuss erfasste Variablen ändern
FnOnceÜbernimmt Eigentum (T)Nur einmalVerbraucht 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

fn main() {
    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

// Definiere eine Funktion, die eine Closure als Parameter entgegennimmt
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

// Gib eine Closure zurück, die den Parameter x erfasst und ihn zum übergebenen Wert addiert
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

fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

fn main() {
    let add_ten = make_adder(10);
    println!("10 + 2 = {}", add_ten(2)); // Ausgabe: 10 + 2 = 12
}
MethodeAllokationsortAnwendbare Szenarien
impl FnStapelDer Closure-Typ kann zur Kompilierungszeit bestimmt werden, was zu besserer Leistung führt
Box<dyn Fn>HeapClosures 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

fn main() {
    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

use std::thread;

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

fn main() {
    let nums = vec![3, -1, 4, -5, 9];

    // Verwende eine Closure, um die erste positive Zahl zu finden
    let first_positive = nums.iter().find(|&amp;&amp;x| x > 0);
    match first_positive {
        Some(&amp;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, &amp;str>> = nums.iter().map(|&amp;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

fn main() {
    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

// Definieren Sie eine Funktion, die eine Closure als Parameter akzeptiert
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