Closures: Anonyme Funktionen, die ihre Umgebung erfassen

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Closures: Anonymous Functions That Capture Their Environment. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab wirst du die Closures in Rust erkunden, die anonymen Funktionen sind, die in Variablen gespeichert oder als Argumente übergeben werden können, was die Code-Wiederverwendung und die Anpassung des Verhaltens ermöglicht, indem sie Werte aus ihrem definierten Bereich aufnehmen.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/mutable_variables -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/integer_types -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/for_loop -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/function_syntax -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/expressions_statements -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/method_syntax -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/traits -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} rust/operator_overloading -.-> lab-100424{{"Closures: Anonyme Funktionen, die ihre Umgebung erfassen"}} end

Closures: Anonymous Functions That Capture Their Environment

Rusts Closures sind anonyme Funktionen, die du in einer Variable speichern oder als Argumente an andere Funktionen übergeben kannst. Du kannst die Closure an einem Ort erstellen und dann an einem anderen Ort aufrufen, um sie in einem anderen Kontext auszuwerten. Im Gegensatz zu Funktionen können Closures Werte aus dem Bereich, in dem sie definiert sind, aufnehmen. Wir werden demonstrieren, wie diese Closure-Features die Code-Wiederverwendung und die Anpassung des Verhaltens ermöglichen.

Das Einfangen der Umgebung mit Closures

Wir werden zunächst untersuchen, wie wir Closures verwenden können, um Werte aus der Umgebung, in der sie definiert sind, zu erfassen und später zu verwenden. Hier ist der Szenario: von Zeit zu Zeit gibt unsere T-Shirt-Firma einen exklusiven, begrenzt editierten Shirt an jemand aus unserer Mailingliste als Werbung aus. Personen auf der Mailingliste können optional ihre Lieblingsfarbe zu ihrem Profil hinzufügen. Wenn die Person, die für ein kostenloses Shirt ausgewählt wird, ihre Lieblingsfarbe angegeben hat, erhält sie das Shirt in dieser Farbe. Wenn die Person keine Lieblingsfarbe angegeben hat, erhält sie die Farbe, von der die Firma derzeit am meisten hat.

Es gibt viele Möglichkeiten, dies zu implementieren. Für dieses Beispiel werden wir eine Enumeration namens ShirtColor verwenden, die die Varianten Red und Blue hat (um die Anzahl der verfügbaren Farben zur Vereinfachung zu begrenzen). Wir repräsentieren den Vorrat der Firma mit einer Struktur Inventory, die ein Feld namens shirts hat, das ein Vec<ShirtColor> enthält, das die derzeit im Lager befindlichen Shirt-Farben darstellt. Die Methode giveaway, die auf Inventory definiert ist, erhält die optionale Shirt-Farben-Präferenz des Gewinners des kostenlosen Shirts und gibt die Shirt-Farbe zurück, die die Person erhält. Diese Einrichtung ist in Listing 13-1 gezeigt.

Dateiname: src/main.rs

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(
        &self,
        user_preference: Option<ShirtColor>,
    ) -> ShirtColor {
      1 user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
      2 shirts: vec![
            ShirtColor::Blue,
            ShirtColor::Red,
            ShirtColor::Blue,
        ],
    };

    let user_pref1 = Some(ShirtColor::Red);
  3 let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
  4 let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

Listing 13-1: Shirt-Firma-Gewinnspiel-Situation

Die store, die in main definiert ist, hat noch zwei blaue Shirts und ein rotes Shirt für diese begrenzt editierten Werbung übrig, die verteilt werden sollen [2]. Wir rufen die Methode giveaway für einen Benutzer auf, der eine Vorliebe für ein rotes Shirt hat [3] und für einen Benutzer ohne jede Vorliebe [4].

Wiederum könnte dieser Code auf viele Weise implementiert werden, und hier, um sich auf Closures zu konzentrieren, haben wir uns an Konzepte gehalten, die Sie bereits gelernt haben, außer für den Körper der giveaway-Methode, der einen Closure verwendet. In der giveaway-Methode erhalten wir die Benutzerpräferenz als Parameter vom Typ Option<ShirtColor> und rufen die Methode unwrap_or_else auf user_preference auf [1]. Die Methode unwrap_or_else auf Option<T> wird von der Standardbibliothek definiert. Sie nimmt ein Argument: einen Closure ohne Argumente, der einen Wert T zurückgibt (der gleiche Typ wie der in der Some-Variante von Option<T> gespeicherte Typ, in diesem Fall ShirtColor). Wenn die Option<T> die Some-Variante ist, gibt unwrap_or_else den Wert aus der Some zurück. Wenn die Option<T> die None-Variante ist, ruft unwrap_or_else den Closure auf und gibt den von dem Closure zurückgegebenen Wert zurück.

Wir geben den Closure-Ausdruck || self.most_stocked() als Argument an unwrap_or_else an. Dies ist ein Closure, das keine Parameter selbst hat (wenn das Closure Parameter hätte, würden sie zwischen den beiden vertikalen Rohren erscheinen). Der Körper des Closures ruft self.most_stocked() auf. Wir definieren hier das Closure, und die Implementierung von unwrap_or_else wird das Closure später auswerten, wenn das Ergebnis benötigt wird.

Wenn Sie diesen Code ausführen, wird Folgendes ausgegeben:

The user with preference Some(Red) gets Red
The user with preference None gets Blue

Ein interessanter Aspekt hier ist, dass wir einen Closure übergeben haben, der self.most_stocked() auf der aktuellen Inventory-Instanz aufruft. Die Standardbibliothek musste nichts über die von uns definierten Typen Inventory oder ShirtColor oder die Logik wissen, die wir in diesem Szenario verwenden möchten. Das Closure fängt eine unveränderliche Referenz auf die self Inventory-Instanz ein und übergibt sie zusammen mit dem von uns angegebenen Code an die Methode unwrap_or_else. Funktionen hingegen können ihre Umgebung nicht auf diese Weise erfassen.

Closure-Typenschlussfolgerung und -Annotation

Es gibt weitere Unterschiede zwischen Funktionen und Closures. Closures erfordern normalerweise nicht, dass Sie die Typen der Parameter oder des Rückgabewerts wie fn-Funktionen annotieren. Typenannotationen sind bei Funktionen erforderlich, weil die Typen Teil einer expliziten Schnittstelle sind, die Ihren Benutzern präsentiert wird. Die strikte Definition dieser Schnittstelle ist wichtig, um sicherzustellen, dass alle übereinstimmen, welche Typen von Werten eine Funktion verwendet und zurückgibt. Closures hingegen werden nicht in einer so offenen Schnittstelle verwendet: Sie werden in Variablen gespeichert und ohne Namensgebung und Offenlegung an die Benutzer unserer Bibliothek verwendet.

Closures sind typischerweise kurz und nur innerhalb eines engen Kontexts relevant, nicht in beliebigen Szenarien. Innerhalb dieser begrenzten Zusammenhänge kann der Compiler die Typen der Parameter und den Rückgabetyp ableiten, ähnlich wie er es für die meisten Variablen kann (es gibt seltene Fälle, in denen der Compiler auch Closure-Typenannotationen benötigt).

Wie bei Variablen können wir Typenannotationen hinzufügen, wenn wir die Exaktheit und Klarheit erhöhen möchten, allerdings auf Kosten einer höheren Wortlautlänge als strikt erforderlich. Die Typenannotation für einen Closure würde wie in Listing 13-2 aussehen. In diesem Beispiel definieren wir einen Closure und speichern es in einer Variable, anstatt wie in Listing 13-1 den Closure direkt an der Stelle zu definieren, wo wir ihn als Argument übergeben.

Dateiname: src/main.rs

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

Listing 13-2: Hinzufügen von optionalen Typenannotationen für die Parameter- und Rückgabetypen im Closure

Mit den hinzugefügten Typenannotationen sieht die Syntax von Closures ähnlicher wie die von Funktionen aus. Hier definieren wir eine Funktion, die 1 zu ihrem Parameter addiert, und einen Closure mit dem gleichen Verhalten, zum Vergleich. Wir haben einige Leerzeichen hinzugefügt, um die relevanten Teile zu aligngen. Dies veranschaulicht, wie die Closure-Syntax ähnlich wie die Funktions-Syntax ist, außer für die Verwendung von Rohren und die Menge an optionaler Syntax:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Die erste Zeile zeigt eine Funktionsdefinition und die zweite Zeile zeigt eine vollständige annotierte Closure-Definition. In der dritten Zeile entfernen wir die Typenannotationen aus der Closure-Definition. In der vierten Zeile entfernen wir die geschweiften Klammern, die optional sind, weil der Closure-Körper nur einen Ausdruck hat. Dies sind alle gültige Definitionen, die das gleiche Verhalten haben, wenn sie aufgerufen werden. Die Zeilen add_one_v3 und add_one_v4 erfordern die Auswertung der Closures, um zu kompilieren, weil die Typen aus ihrer Verwendung abgeleitet werden müssen. Dies ist ähnlich wie let v = Vec::new();, das entweder Typenannotationen oder Werte eines bestimmten Typs benötigt, um in den Vec eingefügt zu werden, damit Rust den Typ ableiten kann.

Für Closure-Definitionen wird der Compiler für jeden ihrer Parameter und für ihren Rückgabetyp einen konkreten Typ ableiten. Beispielsweise zeigt Listing 13-3 die Definition eines kurzen Closures, das einfach den Wert zurückgibt, den es als Parameter erhält. Dieser Closure ist außerhalb dieses Beispiels nicht sehr nützlich. Beachten Sie, dass wir keine Typenannotationen zur Definition hinzugefügt haben. Da es keine Typenannotationen gibt, können wir den Closure mit jedem Typ aufrufen, was wir hier zum ersten Mal mit String getan haben. Wenn wir dann versuchen, example_closure mit einem Integer aufzurufen, erhalten wir einen Fehler.

Dateiname: src/main.rs

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

Listing 13-3: Versuch, einen Closure aufzurufen, dessen Typen aus zwei verschiedenen Typen abgeleitet werden

Der Compiler gibt uns diesen Fehler:

error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |                             ^- help: try using a conversion method:
`.to_string()`
  |                             |
  |                             expected struct `String`, found integer

Als erstes Mal rufen wir example_closure mit dem String-Wert auf. Der Compiler leitet dann den Typ von x und den Rückgabetyp des Closures als String ab. Diese Typen werden dann im Closure in `example_closure festgelegt, und wir erhalten einen Typfehler, wenn wir dann versuchen, einen anderen Typ mit demselben Closure zu verwenden.

Das Einfangen von Referenzen oder die Übergabe der Eigentumsgewalt

Closures können Werte aus ihrer Umgebung auf drei Arten einfangen, was direkt auf die drei Arten abbildet, auf denen eine Funktion einen Parameter entgegennehmen kann: unveränderliche Referenzübernahme, veränderliche Referenzübernahme und die Übernahme der Eigentumsgewalt. Der Closure wird entscheiden, welche dieser Methoden verwendet werden soll, basierend darauf, was der Funktionskörper mit den eingefangenen Werten macht.

In Listing 13-4 definieren wir einen Closure, der eine unveränderliche Referenz auf den Vektor namens list einfangt, da es nur eine unveränderliche Referenz benötigt, um den Wert auszugeben.

Dateiname: src/main.rs

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

  1 let only_borrows = || println!("From closure: {:?}", list);

    println!("Before calling closure: {:?}", list);
  2 only_borrows();
    println!("After calling closure: {:?}", list);
}

Listing 13-4: Definieren und Aufrufen eines Closures, das eine unveränderliche Referenz einfangt

Dieses Beispiel veranschaulicht auch, dass eine Variable an eine Closure-Definition gebunden werden kann [1], und wir können den Closure später aufrufen, indem wir den Variablennamen und Klammern verwenden, als wäre der Variablennamen ein Funktionsname [2].

Da wir gleichzeitig mehrere unveränderliche Referenzen auf list haben können, ist list auch von dem Code vor der Closure-Definition, nach der Closure-Definition aber vor dem Aufruf des Closures und nach dem Aufruf des Closures noch zugänglich. Dieser Code kompiliert, läuft und gibt aus:

Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

Als nächstes ändern wir in Listing 13-5 den Closure-Körper, sodass er ein Element zum list-Vektor hinzufügt. Der Closure fängt jetzt eine veränderliche Referenz ein.

Dateiname: src/main.rs

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {:?}", list);
}

Listing 13-5: Definieren und Aufrufen eines Closures, das eine veränderliche Referenz einfangt

Dieser Code kompiliert, läuft und gibt aus:

Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

Beachten Sie, dass es zwischen der Definition und dem Aufruf des borrows_mutably-Closures keine println! mehr gibt: Wenn borrows_mutably definiert wird, fängt es eine veränderliche Referenz auf list ein. Wir verwenden den Closure nicht mehr nach dem Aufruf des Closures, sodass die veränderliche Referenzende. Zwischen der Closure-Definition und dem Closure-Aufruf ist eine unveränderliche Referenz zum Ausgeben nicht erlaubt, da keine anderen Referenzen erlaubt sind, wenn es eine veränderliche Referenz gibt. Versuchen Sie, eine println! dort hinzuzufügen, um zu sehen, welche Fehlermeldung Sie erhalten!

Wenn Sie den Closure erzwingen möchten, die Werte in der Umgebung, die es verwendet, in die Eigentumsgewalt zu übernehmen, auch wenn der Funktionskörper die Eigentumsgewalt nicht streng benötigt, können Sie das Schlüsselwort move vor der Parameterliste verwenden.

Diese Technik ist hauptsächlich nützlich, wenn ein Closure an einen neuen Thread übergeben wird, um die Daten zu verschieben, sodass sie vom neuen Thread besessen werden. Wir werden in Kapitel 16 im Detail über Threads und warum Sie sie verwenden möchten sprechen, wenn wir über Konkurrenz sprechen, aber für jetzt wollen wir kurz untersuchen, wie ein neuer Thread mit einem Closure erzeugt wird, das das move-Schlüsselwort benötigt. Listing 13-6 zeigt Listing 13-4 modifiziert, um den Vektor in einem neuen Thread statt im Hauptthread auszugeben.

Dateiname: src/main.rs

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

  1 thread::spawn(move || {
      2 println!("From thread: {:?}", list)
    }).join().unwrap();
}

Listing 13-6: Verwenden von move, um zu erzwingen, dass der Closure für den Thread die Eigentumsgewalt an list übernimmt

Wir erzeugen einen neuen Thread und übergeben dem Thread ein Closure, das als Argument ausgeführt werden soll. Der Closure-Körper gibt die Liste aus. In Listing 13-4 hat der Closure nur list mit einer unveränderlichen Referenz eingefangen, da das die geringste Zugangsmöglichkeit zu list ist, die zum Ausgeben erforderlich ist. In diesem Beispiel muss der Closure-Körper zwar immer noch nur eine unveränderliche Referenz benötigen [2], aber wir müssen angeben, dass list in den Closure verschoben werden soll, indem wir das move-Schlüsselwort [1] am Anfang der Closure-Definition setzen. Der neue Thread kann vor dem Rest des Hauptthreads fertig sein, oder der Hauptthread kann zuerst fertig werden. Wenn der Hauptthread die Eigentumsgewalt an list behält, aber vor dem neuen Thread endet und list freigibt, wäre die unveränderliche Referenz im Thread ungültig. Daher erfordert der Compiler, dass list in den an den neuen Thread übergebenen Closure verschoben wird, damit die Referenz gültig ist. Versuchen Sie, das move-Schlüsselwort zu entfernen oder list im Hauptthread nach der Closure-Definition zu verwenden, um zu sehen, welche Compilerfehler Sie erhalten!

Das Entfernen von eingefangenen Werten aus Closures und die Fn-Traits

Sobald ein Closure eine Referenz eingefangen hat oder die Eigentumsgewalt an einen Wert aus der Umgebung übernommen hat, in der das Closure definiert ist (was somit beeinflusst, was, wenn überhaupt etwas, in das Closure eingezogen wird), definiert der Code im Körper des Closures, was mit den Referenzen oder Werten passiert, wenn das Closure später ausgewertet wird (was somit beeinflusst, was, wenn überhaupt etwas, aus dem Closure entfernt wird).

Ein Closure-Körper kann eines der folgenden tun: einen eingefangenen Wert aus dem Closure entfernen, den eingefangenen Wert mutieren, weder den Wert entfernen noch mutieren oder überhaupt nichts aus der Umgebung einfangen.

Die Art, wie ein Closure Werte aus der Umgebung einfängt und behandelt, beeinflusst, welche Traits das Closure implementiert, und Traits sind die Art, wie Funktionen und Structs angeben können, welche Arten von Closures sie verwenden können. Closures werden automatisch einen, zwei oder alle drei dieser Fn-Traits in additiver Weise implementieren, je nachdem, wie der Closure-Körper die Werte behandelt:

  • FnOnce gilt für Closures, die einmal aufgerufen werden können. Alle Closures implementieren mindestens diesen Trait, weil alle Closures aufgerufen werden können. Ein Closure, das eingefangene Werte aus seinem Körper entfernt, wird nur FnOnce implementieren und keine der anderen Fn-Traits, da es nur einmal aufgerufen werden kann.
  • FnMut gilt für Closures, die eingefangene Werte nicht aus ihrem Körper entfernen, aber die eingefangenen Werte mutieren können. Diese Closures können mehr als einmal aufgerufen werden.
  • Fn gilt für Closures, die eingefangene Werte nicht aus ihrem Körper entfernen und die eingefangenen Werte nicht mutieren, sowie für Closures, die nichts aus ihrer Umgebung einfangen. Diese Closures können mehr als einmal ohne die Umgebung zu mutieren aufgerufen werden, was wichtig ist, z.B. bei der gleichzeitigen mehrfachen Ausführung eines Closures.

Schauen wir uns die Definition der unwrap_or_else-Methode auf Option<T> an, die wir in Listing 13-1 verwendet haben:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

Denken Sie daran, dass T der generische Typ ist, der den Typ des Werts in der Some-Variante einer Option repräsentiert. Dieser Typ T ist auch der Rückgabetyp der unwrap_or_else-Funktion: Code, der unwrap_or_else auf einer Option<String> aufruft, bekommt beispielsweise einen String.

Als nächstes bemerken Sie, dass die unwrap_or_else-Funktion den zusätzlichen generischen Typparameter F hat. Der F-Typ ist der Typ des Parameters namens f, der das Closure ist, das wir bei der Aufruf von unwrap_or_else angeben.

Die Trait-Bedingung, die auf dem generischen Typ F angegeben ist, lautet FnOnce() -> T, was bedeutet, dass F einmal aufgerufen werden muss, keine Argumente entgegennehmen darf und einen T zurückgeben muss. Die Verwendung von FnOnce in der Trait-Bedingung drückt die Einschränkung aus, dass unwrap_or_else f höchstens einmal aufrufen wird. Im Körper von unwrap_or_else können wir sehen, dass wenn die Option Some ist, f nicht aufgerufen wird. Wenn die Option None ist, wird f einmal aufgerufen. Da alle Closures FnOnce implementieren, akzeptiert unwrap_or_else die größte Anzahl von Closures und ist so flexibel wie möglich.

Hinweis: Funktionen können auch alle drei der Fn-Traits implementieren. Wenn das, was wir tun möchten, nicht erfordert, einen Wert aus der Umgebung einzufangen, können wir den Namen einer Funktion statt eines Closures verwenden, wenn wir etwas benötigen, das einen der Fn-Traits implementiert. Beispielsweise könnten wir auf einem Option<Vec<T>>-Wert unwrap_or_else(Vec::new) aufrufen, um einen neuen, leeren Vektor zu erhalten, wenn der Wert None ist.

Lassen Sie uns jetzt die Standardbibliotheksmethode sort_by_key betrachten, die auf Slices definiert ist, um zu sehen, wie das von unwrap_or_else unterschiedlich ist und warum sort_by_key für die Trait-Bedingung FnMut statt FnOnce verwendet. Das Closure erhält ein Argument in Form einer Referenz auf das aktuelle Element im betrachteten Slice und gibt einen Wert vom Typ K zurück, der geordnet werden kann. Diese Funktion ist nützlich, wenn Sie einen Slice nach einem bestimmten Attribut jedes Elements sortieren möchten. In Listing 13-7 haben wir eine Liste von Rectangle-Instanzen und verwenden sort_by_key, um sie nach ihrem width-Attribut von niedrig nach hoch zu ordnen.

Dateiname: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

Listing 13-7: Verwenden von sort_by_key, um Rechtecke nach der Breite zu ordnen

Dieser Code gibt aus:

[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

Der Grund, warum sort_by_key so definiert ist, dass es ein FnMut-Closure annimmt, ist, dass es das Closure mehr als einmal aufruft: einmal für jedes Element im Slice. Das Closure |r| r.width fängt, mutiert oder entfernt nichts aus seiner Umgebung, sodass es die Trait-Bedingungen erfüllt.

Im Gegensatz dazu zeigt Listing 13-8 ein Beispiel für ein Closure, das nur den FnOnce-Trait implementiert, weil es einen Wert aus der Umgebung entfernt. Der Compiler lässt uns dieses Closure nicht mit sort_by_key verwenden.

Dateiname: src/main.rs

--snip--

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

Listing 13-8: Versuch, ein FnOnce-Closure mit sort_by_key zu verwenden

Dies ist eine absichtlich komplizierte und umständliche Weise (die nicht funktioniert), um zu versuchen, die Anzahl der Aufrufe von sort_by_key zu zählen, wenn list sortiert wird. Dieser Code versucht, diese Zählung durch das Hinzufügen von value - einem String aus der Umgebung des Closures - in den sort_operations-Vektor durchzuführen. Das Closure fängt value ein und entfernt dann value aus dem Closure, indem es die Eigentumsgewalt von value an den sort_operations-Vektor übergibt. Dieses Closure kann einmal aufgerufen werden; das Versuchen, es eine zweite Zeit aufzurufen, würde nicht funktionieren, weil value nicht mehr in der Umgebung wäre, um erneut in sort_operations eingefügt zu werden! Daher implementiert dieses Closure nur FnOnce. Wenn wir diesen Code versuchen, zu kompilieren, erhalten wir diesen Fehler, dass value nicht aus dem Closure entfernt werden kann, weil das Closure FnMut implementieren muss:

error[E0507]: cannot move out of `value`, a captured variable in an `FnMut`
closure
  --> src/main.rs:18:30
   |
15 |       let value = String::from("by key called");
   |           ----- captured outer variable
16 |
17 |       list.sort_by_key(|r| {
   |  ______________________-
18 | |         sort_operations.push(value);
   | |                              ^^^^^ move occurs because `value` has
type `String`, which does not implement the `Copy` trait
19 | |         r.width
20 | |     });
   | |_____- captured by this `FnMut` closure

Der Fehler verweist auf die Zeile im Closure-Körper, in der value aus der Umgebung entfernt wird. Um das zu beheben, müssen wir den Closure-Körper ändern, sodass er keine Werte aus der Umgebung entfernt. Ein Zähler in der Umgebung zu halten und seinen Wert im Closure-Körper zu erhöhen, ist eine einfacherere Weise, um die Anzahl der Aufrufe von sort_by_key zu zählen. Das Closure in Listing 13-9 funktioniert mit sort_by_key, weil es nur eine veränderliche Referenz auf den num_sort_operations-Zähler fängt und daher mehr als einmal aufgerufen werden kann.

Dateiname: src/main.rs

--snip--

fn main() {
    --snip--

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!(
        "{:#?}, sorted in {num_sort_operations} operations",
        list
    );
}

Listing 13-9: Das Verwenden eines FnMut-Closures mit sort_by_key ist erlaubt.

Die Fn-Traits sind wichtig, wenn Sie Funktionen oder Typen definieren oder verwenden, die Closures nutzen. Im nächsten Abschnitt werden wir Iteratoren diskutieren. Viele Iterator-Methoden nehmen Closure-Argumente an, also halten Sie diese Closure-Details im Kopf, wenn wir fortfahren!

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab zu Closures: Anonyme Funktionen, die ihre Umgebung erfassen, abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.