Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Domknięcia

Domknięcia w Rust to anonimowe funkcje, które można zapisać w zmiennej lub przekazać jako argumenty do innych funkcji. Można utworzyć domknięcie w jednym miejscu, a następnie wywołać je w innym, aby je ocenić w innym kontekście. W przeciwieństwie do funkcji, domknięcia mogą przechwytywać wartości ze środowiska, w którym są zdefiniowane. Zdemonstrujemy, jak te cechy domknięć pozwalają na ponowne użycie kodu i dostosowanie zachowania.

Przechwytywanie środowiska

Najpierw zbadamy, jak możemy używać domknięć do przechwytywania wartości ze środowiska, w którym są zdefiniowane, do późniejszego użycia. Oto scenariusz: Co jakiś czas nasza firma produkująca koszulki rozdaje ekskluzywną koszulkę z limitowanej edycji osobie z naszej listy mailingowej w ramach promocji. Osoby z listy mailingowej mogą opcjonalnie dodać swój ulubiony kolor do swojego profilu. Jeśli osoba wybrana do darmowej koszulki ma ustawiony ulubiony kolor, otrzymuje koszulkę w tym kolorze. Jeśli osoba nie określiła ulubionego koloru, otrzymuje koszulkę w kolorze, którego firma ma obecnie najwięcej.

Istnieje wiele sposobów na zaimplementowanie tego. W tym przykładzie użyjemy wyliczenia ShirtColor, które ma warianty Red i Blue (dla uproszczenia ograniczamy liczbę dostępnych kolorów). Reprezentujemy stan magazynowy firmy za pomocą struktury Inventory, która ma pole shirts zawierające Vec<ShirtColor> reprezentujące kolory koszulek aktualnie w magazynie. Metoda giveaway zdefiniowana dla Inventory pobiera opcjonalne preferencje koloru koszulki zwycięzcy darmowej koszulki i zwraca kolor koszulki, którą osoba otrzyma. Ta konfiguracja jest pokazana w Listing 13-1.

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

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        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 {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "Użytkownik z preferencją {:?} dostaje {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "Użytkownik z preferencją {:?} dostaje {:?}",
        user_pref2, giveaway2
    );
}

store zdefiniowany w main ma dwie niebieskie i jedną czerwoną koszulkę do rozdania w ramach tej limitowanej promocji. Wywołujemy metodę giveaway dla użytkownika preferującego czerwoną koszulkę i dla użytkownika bez żadnych preferencji.

Ponownie, ten kod mógłby być zaimplementowany na wiele sposobów, a tutaj, aby skupić się na domknięciach, trzymaliśmy się pojęć, które już znasz, z wyjątkiem ciała metody giveaway, która używa domknięcia. W metodzie giveaway otrzymujemy preferencję użytkownika jako parametr typu Option<ShirtColor> i wywołujemy metodę unwrap_or_else na user_preference. Metoda unwrap_or_else w Option<T> jest zdefiniowana przez standardową bibliotekę. Przyjmuje jeden argument: domknięcie bez żadnych argumentów, które zwraca wartość T (tego samego typu co przechowywany w wariancie Some Option<T>, w tym przypadku ShirtColor). Jeśli Option<T> jest wariantem Some, unwrap_or_else zwraca wartość z Some. Jeśli Option<T> jest wariantem None, unwrap_or_else wywołuje domknięcie i zwraca wartość zwróconą przez domknięcie.

Określamy wyrażenie domknięcia || self.most_stocked() jako argument dla unwrap_or_else. Jest to domknięcie, które samo nie przyjmuje parametrów (gdyby domknięcie miało parametry, pojawiłyby się one między dwoma pionowymi kreskami). Ciało domknięcia wywołuje self.most_stocked(). Definiujemy tutaj domknięcie, a implementacja unwrap_or_else oceni domknięcie później, jeśli wynik będzie potrzebny.

Uruchomienie tego kodu wyświetli następujące informacje:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
Użytkownik z preferencją Some(Red) dostaje Red
Użytkownik z preferencją None dostaje Blue

Jednym interesującym aspektem jest to, że przekazaliśmy domknięcie, które wywołuje self.most_stocked() na bieżącej instancji Inventory. Standardowa biblioteka nie musiała wiedzieć nic o typach Inventory ani ShirtColor, które zdefiniowaliśmy, ani o logice, której chcemy użyć w tym scenariuszu. Domknięcie przechwytuje niezmienną referencję do instancji self Inventory i przekazuje ją z określonym przez nas kodem do metody unwrap_or_else. Funkcje natomiast nie są w stanie przechwytywać swojego środowiska w ten sposób.

Wywnioskowanie i adnotowanie typów domknięć

Istnieje więcej różnic między funkcjami a domknięciami. Domknięcia zazwyczaj nie wymagają adnotowania typów parametrów ani wartości zwracanej, tak jak funkcje fn. Adnotacje typów są wymagane w funkcjach, ponieważ typy są częścią jawnego interfejsu udostępnianego użytkownikom. Sztywne definiowanie tego interfejsu jest ważne dla zapewnienia, że wszyscy zgadzają się co do tego, jakich typów wartości funkcja używa i zwraca. Domknięcia natomiast nie są używane w takim udostępnionym interfejsie: są przechowywane w zmiennych i używane bez nazywania ich i udostępniania użytkownikom naszej biblioteki.

Domknięcia są zazwyczaj krótkie i istotne tylko w wąskim kontekście, a nie w dowolnym scenariuszu. W tych ograniczonych kontekstach kompilator może wywnioskować typy parametrów i typ zwracany, podobnie jak jest w stanie wywnioskować typy większości zmiennych (istnieją rzadkie przypadki, w których kompilator również potrzebuje adnotacji typów domknięć).

Podobnie jak w przypadku zmiennych, możemy dodać adnotacje typów, jeśli chcemy zwiększyć jawność i klarowność kosztem większej szczegółowości, niż jest to ściśle konieczne. Adnotowanie typów dla domknięcia wyglądałoby jak definicja pokazana w Listing 13-2. W tym przykładzie definiujemy domknięcie i przechowujemy je w zmiennej, zamiast definiować domknięcie w miejscu, w którym przekazujemy je jako argument, jak to zrobiliśmy w Listing 13-1.

use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        println!("obliczanie powoli...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Dziś, zrób {} pompek!", expensive_closure(intensity));
        println!("Następnie, zrób {} brzuszków!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Zrób sobie dziś przerwę! Pamiętaj o nawodnieniu!");
        } else {
            println!(
                "Dziś, biegnij przez {} minut!",
                expensive_closure(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}

Po dodaniu adnotacji typów składnia domknięć wygląda bardziej podobnie do składni funkcji. Tutaj definiujemy funkcję, która dodaje 1 do swojego parametru, i domknięcie, które ma takie samo zachowanie, dla porównania. Dodaliśmy kilka spacji, aby wyrównać odpowiednie części. Pokazuje to, jak składnia domknięcia jest podobna do składni funkcji, z wyjątkiem użycia pionowych kresek i ilości składni, która jest opcjonalna:

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  ;

Pierwsza linia pokazuje definicję funkcji, a druga definicję domknięcia z pełnymi adnotacjami. W trzeciej linii usuwamy adnotacje typów z definicji domknięcia. W czwartej linii usuwamy nawiasy klamrowe, które są opcjonalne, ponieważ ciało domknięcia ma tylko jedno wyrażenie. Wszystkie te definicje są poprawne i będą dawać to samo zachowanie po ich wywołaniu. Linie add_one_v3 i add_one_v4 wymagają oceny domknięć, aby mogły się skompilować, ponieważ typy zostaną wywnioskowane z ich użycia. Jest to podobne do let v = Vec::new();, które wymaga albo adnotacji typów, albo wartości jakiegoś typu do wstawienia do Vec, aby Rust mógł wywnioskować typ.

Dla definicji domknięć kompilator wywnioskuje jeden konkretny typ dla każdego z ich parametrów i dla ich wartości zwracanej. Na przykład, Listing 13-3 pokazuje definicję krótkiego domknięcia, które po prostu zwraca wartość, którą otrzymuje jako parametr. To domknięcie nie jest zbyt użyteczne, z wyjątkiem celów tego przykładu. Zauważ, że nie dodaliśmy żadnych adnotacji typów do definicji. Ponieważ nie ma adnotacji typów, możemy wywołać domknięcie z dowolnym typem, co zrobiliśmy tutaj z String za pierwszym razem. Jeśli następnie spróbujemy wywołać example_closure z liczbą całkowitą, otrzymamy błąd.

fn main() {
    let example_closure = |x| x;

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

Kompilator daje nam następujący błąd:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^ expected `String`, found integer
  |             |
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^
help: try using a conversion method
  |
5 |     let n = example_closure(5.to_string());
  |                              ++++++++++++

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

Za pierwszym razem, gdy wywołujemy example_closure z wartością String, kompilator wywnioskowuje typ x i typ zwracany domknięcia jako String. Te typy są następnie zablokowane w domknięciu w example_closure, i otrzymujemy błąd typu, gdy następnym razem próbujemy użyć innego typu z tym samym domknięciem.

Przechwytywanie referencji lub przenoszenie własności

Domknięcia mogą przechwytywać wartości ze swojego środowiska na trzy sposoby, które bezpośrednio odpowiadają trzem sposobom, w jakie funkcja może przyjąć parametr: pożyczanie niezmienne, pożyczanie zmienne i przejmowanie własności. Domknięcie zdecyduje, którego z nich użyć, w zależności od tego, co ciało funkcji robi z przechwyconymi wartościami.

W Listing 13-4 definiujemy domknięcie, które przechwytuje niezmienną referencję do wektora o nazwie list, ponieważ potrzebuje jedynie niezmiennej referencji, aby wydrukować wartość.

fn main() {
    let list = vec![1, 2, 3];
    println!("Przed zdefiniowaniem domknięcia: {list:?}");

    let only_borrows = || println!("Z domknięcia: {list:?}");

    println!("Przed wywołaniem domknięcia: {list:?}");
    only_borrows();
    println!("Po wywołaniu domknięcia: {list:?}");
}

Ten przykład ilustruje również, że zmienna może być związana z definicją domknięcia, a później możemy wywołać domknięcie, używając nazwy zmiennej i nawiasów, tak jakby nazwa zmiennej była nazwą funkcji.

Ponieważ możemy mieć jednocześnie wiele niezmiennych referencji do list, list jest nadal dostępny z kodu przed definicją domknięcia, po definicji domknięcia, ale przed wywołaniem domknięcia, i po wywołaniu domknięcia. Ten kod kompiluje się, uruchamia i drukuje:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Przed zdefiniowaniem domknięcia: [1, 2, 3]
Przed wywołaniem domknięcia: [1, 2, 3]
Z domknięcia: [1, 2, 3]
Po wywołaniu domknięcia: [1, 2, 3]

Następnie, w Listing 13-5, zmieniamy ciało domknięcia tak, aby dodawało element do wektora list. Domknięcie teraz przechwytuje zmienną referencję.

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Przed zdefiniowaniem domknięcia: {list:?}");

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

    borrows_mutably();
    println!("Po wywołaniu domknięcia: {list:?}");
}

Ten kod kompiluje się, uruchamia i drukuje:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Przed zdefiniowaniem domknięcia: [1, 2, 3]
Po wywołaniu domknięcia: [1, 2, 3, 7]

Zauważ, że nie ma już println! między definicją a wywołaniem domknięcia borrows_mutably: Kiedy borrows_mutably jest zdefiniowane, przechwytuje zmienną referencję do list. Nie używamy domknięcia ponownie po jego wywołaniu, więc zmienna pożyczka się kończy. Między definicją domknięcia a wywołaniem domknięcia, niezmienna pożyczka do wydrukowania nie jest dozwolona, ponieważ żadne inne pożyczki nie są dozwolone, gdy istnieje zmienna pożyczka. Spróbuj dodać tam println!, aby zobaczyć, jaki komunikat o błędzie otrzymasz!

Jeśli chcesz wymusić na domknięciu przejęcie własności wartości, których używa w środowisku, mimo że ciało domknięcia nie potrzebuje ściśle własności, możesz użyć słowa kluczowego move przed listą parametrów.

Ta technika jest najbardziej użyteczna, gdy przekazujemy domknięcie do nowego wątku, aby przenieść dane, tak aby nowy wątek był ich właścicielem. Szczegółowo omówimy wątki i dlaczego warto ich używać w Rozdziale 16, kiedy będziemy mówić o współbieżności, ale na razie przyjrzyjmy się krótko tworzeniu nowego wątku za pomocą domknięcia, które wymaga słowa kluczowego move. Listing 13-6 pokazuje Listing 13-4 zmodyfikowany tak, aby drukował wektor w nowym wątku, a nie w wątku głównym.

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Przed zdefiniowaniem domknięcia: {list:?}");

    thread::spawn(move || println!("Z wątku: {list:?}"))
        .join()
        .unwrap();
}

Tworzymy nowy wątek, przekazując mu domknięcie do uruchomienia jako argument. Ciało domknięcia wypisuje listę. W Listing 13-4 domknięcie przechwytywało list tylko za pomocą niezmiennej referencji, ponieważ to jest minimalny dostęp do list potrzebny do jego wydrukowania. W tym przykładzie, mimo że ciało domknięcia nadal potrzebuje tylko niezmiennej referencji, musimy określić, że list powinno zostać przeniesione do domknięcia, umieszczając słowo kluczowe move na początku definicji domknięcia. Gdyby wątek główny wykonywał więcej operacji przed wywołaniem join na nowym wątku, nowy wątek mógłby zakończyć się przed zakończeniem reszty wątku głównego, lub wątek główny mógłby zakończyć się pierwszy. Gdyby wątek główny zachował własność list, ale zakończył się przed nowym wątkiem i upuścił list, niezmienna referencja w wątku byłaby nieważna. Dlatego kompilator wymaga przeniesienia list do domknięcia przekazanego nowemu wątkowi, aby referencja była ważna. Spróbuj usunąć słowo kluczowe move lub użyć list w wątku głównym po zdefiniowaniu domknięcia, aby zobaczyć, jakie błędy kompilatora otrzymasz!

Przenoszenie przechwyconych wartości poza domknięcia

Gdy domknięcie przechwyci referencję lub własność wartości ze środowiska, w którym jest zdefiniowane (wpływając tym samym na to, co, jeśli w ogóle, jest przenoszone do domknięcia), kod w ciele domknięcia określa, co dzieje się z referencjami lub wartościami, gdy domknięcie jest później oceniane (wpływając tym samym na to, co, jeśli w ogóle, jest przenoszone z domknięcia).

Ciało domknięcia może wykonać dowolne z następujących czynności: przenieść przechwyconą wartość poza domknięcie, zmutować przechwyconą wartość, ani nie przenieść, ani nie zmutować wartości, lub w ogóle nic nie przechwycić ze środowiska.

Sposób, w jaki domknięcie przechwytuje i obsługuje wartości ze środowiska, wpływa na to, które cechy domknięcie implementuje, a cechy to sposób, w jaki funkcje i struktury mogą określać, jakich rodzajów domknięć mogą używać. Domknięcia automatycznie zaimplementują jedną, dwie lub wszystkie trzy z tych cech Fn, w sposób addytywny, w zależności od tego, jak ciało domknięcia obsługuje wartości:

  • FnOnce dotyczy domknięć, które można wywołać raz. Wszystkie domknięcia implementują co najmniej tę cechę, ponieważ wszystkie domknięcia można wywołać. Domknięcie, które przenosi przechwycone wartości ze swojego ciała, zaimplementuje tylko FnOnce i żadnych innych cech Fn, ponieważ można je wywołać tylko raz.
  • FnMut dotyczy domknięć, które nie przenoszą przechwyconych wartości ze swojego ciała, ale mogą mutować przechwycone wartości. Te domknięcia można wywołać więcej niż raz.
  • Fn dotyczy domknięć, które nie przenoszą przechwyconych wartości ze swojego ciała i nie mutują przechwyconych wartości, a także domknięć, które nic nie przechwytują ze swojego środowiska. Te domknięcia można wywołać więcej niż raz bez mutowania ich środowiska, co jest ważne w przypadkach takich jak wielokrotne wywoływanie domknięcia współbieżnie.

Przyjrzyjmy się definicji metody unwrap_or_else na Option<T>, której użyliśmy w Listing 13-1:

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(),
        }
    }
}

Pamiętaj, że T to typ ogólny reprezentujący typ wartości w wariancie Some Option. Ten typ T jest również typem zwracanym przez funkcję unwrap_or_else: kod, który wywołuje unwrap_or_else na Option<String>, na przykład, otrzyma String.

Następnie, zauważ, że funkcja unwrap_or_else ma dodatkowy generyczny parametr typu F. Typ F to typ parametru o nazwie f, czyli domknięcia, które dostarczamy podczas wywoływania unwrap_or_else.

Ograniczenie cechy określone dla typu generycznego F to FnOnce() -> T, co oznacza, że F musi być wywoływalne raz, nie przyjmować żadnych argumentów i zwracać T. Użycie FnOnce w ograniczeniu cechy wyraża ograniczenie, że unwrap_or_else nie wywoła f więcej niż raz. W ciele unwrap_or_else widzimy, że jeśli Option jest Some, f nie zostanie wywołane. Jeśli Option jest None, f zostanie wywołane raz. Ponieważ wszystkie domknięcia implementują FnOnce, unwrap_or_else akceptuje wszystkie trzy rodzaje domknięć i jest tak elastyczne, jak to tylko możliwe.

Uwaga: Jeśli to, co chcemy zrobić, nie wymaga przechwytywania wartości ze środowiska, możemy użyć nazwy funkcji zamiast domknięcia tam, gdzie potrzebujemy czegoś, co implementuje jedną z cech Fn. Na przykład, na wartości Option<Vec<T>> moglibyśmy wywołać unwrap_or_else(Vec::new), aby otrzymać nowy, pusty wektor, jeśli wartość to None. Kompilator automatycznie implementuje dowolną z cech Fn, która jest odpowiednia dla definicji funkcji.

Teraz przyjrzyjmy się metodzie sort_by_key ze standardowej biblioteki, zdefiniowanej dla wycinków, aby zobaczyć, jak różni się ona od unwrap_or_else i dlaczego sort_by_key używa FnMut zamiast FnOnce dla ograniczenia cechy. Domknięcie otrzymuje jeden argument w postaci referencji do bieżącego elementu w rozpatrywanym wycinku i zwraca wartość typu K, którą można posortować. Ta funkcja jest przydatna, gdy chcesz posortować wycinek według określonego atrybutu każdego elementu. W Listing 13-7 mamy listę instancji Rectangle i używamy sort_by_key do uporządkowania ich według atrybutu width od najmniejszego do największego.

#[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:#?}");
}

Ten kod drukuje:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

Powodem, dla którego sort_by_key jest zdefiniowany tak, aby przyjmować domknięcie FnMut, jest to, że wywołuje ono domknięcie wielokrotnie: raz dla każdego elementu w wycinku. Domknięcie |r| r.width nie przechwytuje, nie mutuje ani nie przenosi niczego ze swojego środowiska, więc spełnia wymagania ograniczenia cechy.

Natomiast Listing 13-8 przedstawia przykład domknięcia, które implementuje tylko cechę FnOnce, ponieważ przenosi wartość ze środowiska. Kompilator nie pozwoli nam użyć tego domknięcia z sort_by_key.

#[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 },
    ];

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

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

To jest wymyślny, skomplikowany sposób (który nie działa) na próbę zliczenia, ile razy sort_by_key wywołuje domknięcie podczas sortowania list. Ten kod próbuje to zrobić, wpychając valueString ze środowiska domknięcia – do wektora sort_operations. Domknięcie przechwytuje value, a następnie przenosi value poza domknięcie, przekazując własność value do wektora sort_operations. To domknięcie można wywołać raz; próba wywołania go po raz drugi nie zadziała, ponieważ value nie byłoby już w środowisku, aby ponownie wpychać je do sort_operations! Dlatego to domknięcie implementuje tylko FnOnce. Kiedy próbujemy skompilować ten kod, otrzymujemy następujący błąd, że value nie może zostać przeniesione poza domknięcie, ponieważ domknięcie musi implementować FnMut:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         -----   ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |         |
   |         captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ `value` is moved here
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

Błąd wskazuje na linię w ciele domknięcia, która przenosi value poza środowisko. Aby to naprawić, musimy zmienić ciało domknięcia tak, aby nie przenosiło wartości poza środowisko. Utrzymywanie licznika w środowisku i inkrementowanie jego wartości w ciele domknięcia jest prostszym sposobem na zliczanie, ile razy domknięcie jest wywoływane. Domknięcie w Listing 13-9 działa z sort_by_key, ponieważ przechwytuje tylko zmienną referencję do licznika num_sort_operations i dlatego może być wywoływane więcej niż raz.

#[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 },
    ];

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

Cechy Fn są ważne podczas definiowania lub używania funkcji lub typów, które wykorzystują domknięcia. W następnej sekcji omówimy iteratory. Wiele metod iteratora przyjmuje argumenty domknięcia, więc pamiętaj o tych szczegółach domknięć, kontynuując!