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

Zaawansowane Funkcje i Domknięcia

Ta sekcja omawia niektóre zaawansowane funkcje związane z funkcjami i domknięciami, w tym wskaźniki na funkcje i zwracanie domknięć.

Wskaźniki na Funkcje

Mówiliśmy o tym, jak przekazywać domknięcia do funkcji; możesz również przekazywać do funkcji zwykłe funkcje! Ta technika jest przydatna, gdy chcesz przekazać funkcję, którą już zdefiniowałeś, zamiast definiować nowe domknięcie. Funkcje konwertują się do typu fn (z małą literą f), nie mylić z cechą domknięcia Fn. Typ fn nazywany jest wskaźnikiem na funkcję. Przekazywanie funkcji za pomocą wskaźników na funkcje umożliwi ci używanie funkcji jako argumentów dla innych funkcji.

Składnia określania, że parametr jest wskaźnikiem na funkcję, jest podobna do tej dla domknięć, jak pokazano w Listing 20-28, gdzie zdefiniowaliśmy funkcję add_one, która dodaje 1 do swojego parametru. Funkcja do_twice przyjmuje dwa parametry: wskaźnik na funkcję do dowolnej funkcji, która przyjmuje parametr i32 i zwraca i32, oraz jedną wartość i32. Funkcja do_twice wywołuje funkcję f dwukrotnie, przekazując jej wartość arg, a następnie dodaje do siebie wyniki dwóch wywołań funkcji. Funkcja main wywołuje do_twice z argumentami add_one i 5.

Filename: src/main.rs
fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}
Listing 20-28: Użycie typu fn do przyjęcia wskaźnika na funkcję jako argumentu

Ten kod wyświetla The answer is: 12. Określamy, że parametr f w do_twice jest typu fn, który przyjmuje jeden parametr typu i32 i zwraca i32. Następnie możemy wywołać f w ciele do_twice. W main możemy przekazać nazwę funkcji add_one jako pierwszy argument do do_twice.

W przeciwieństwie do domknięć, fn jest typem, a nie cechą, więc określamy fn jako typ parametru bezpośrednio, zamiast deklarować parametr typu generycznego z jedną z cech domknięć jako ograniczeniem cechy.

Wskaźniki na funkcje implementują wszystkie trzy cechy domknięć (Fn, FnMut i FnOnce), co oznacza, że zawsze możesz przekazać wskaźnik na funkcję jako argument do funkcji, która oczekuje domknięcia. Najlepiej jest pisać funkcje, używając typu generycznego i jednej z cech domknięć, aby twoje funkcje mogły przyjmować zarówno funkcje, jak i domknięcia.

Jednakże, jednym z przykładów, gdzie chciałbyś akceptować tylko fn, a nie domknięcia, jest interakcja z zewnętrznym kodem, który nie ma domknięć: Funkcje w C mogą przyjmować funkcje jako argumenty, ale C nie ma domknięć.

Jako przykład, gdzie można by użyć zarówno domknięcia zdefiniowanego inline, jak i nazwanej funkcji, przyjrzyjmy się zastosowaniu metody map dostarczonej przez cechę Iterator w standardowej bibliotece. Aby użyć metody map do przekształcenia wektora liczb w wektor ciągów znaków, moglibyśmy użyć domknięcia, jak w Listing 20-29.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}
Listing 20-29: Użycie domknięcia z metodą map do konwersji liczb na ciągi znaków

Albo moglibyśmy nazwać funkcję jako argument do map zamiast domknięcia. Listing 20-30 pokazuje, jak by to wyglądało.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}
Listing 20-30: Użycie funkcji String::to_string z metodą map do konwersji liczb na ciągi znaków

Zauważ, że musimy użyć w pełni kwalifikowanej składni, o której mówiliśmy w sekcji „Zaawansowane Cechy”, ponieważ istnieje wiele dostępnych funkcji o nazwie to_string.

Tutaj używamy funkcji to_string zdefiniowanej w cesze ToString, którą standardowa biblioteka zaimplementowała dla każdego typu, który implementuje Display.

Przypomnij sobie z sekcji „Wartości enum” w Rozdziale 6, że nazwa każdego wariantu enum, który definiujemy, staje się także funkcją inicjalizującą. Możemy używać tych funkcji inicjalizujących jako wskaźników na funkcje, które implementują cechy domknięć, co oznacza, że możemy określać funkcje inicjalizujące jako argumenty dla metod, które przyjmują domknięcia, jak pokazano w Listing 20-31.

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
Listing 20-31: Użycie inicjalizatora enum z metodą map do tworzenia instancji Status z liczb

Tutaj tworzymy instancje Status::Value używając każdej wartości u32 w zakresie, na którym wywołano map, poprzez użycie funkcji inicjalizującej Status::Value. Niektórzy wolą ten styl, a inni wolą używać domknięć. Kompilują się one do tego samego kodu, więc używaj stylu, który jest dla ciebie jaśniejszy.

Zwracanie Domknięć

Domknięcia są reprezentowane przez cechy, co oznacza, że nie można zwracać domknięć bezpośrednio. W większości przypadków, gdy chcesz zwrócić cechę, możesz zamiast tego użyć konkretnego typu, który implementuje tę cechę, jako wartości zwracanej funkcji. Jednakże, zazwyczaj nie możesz tego zrobić z domknięciami, ponieważ nie mają one konkretnego typu, który można zwrócić; nie wolno używać wskaźnika na funkcję fn jako typu zwracanego, jeśli domknięcie przechwytuje jakiekolwiek wartości ze swojego zasięgu, na przykład.

Zamiast tego, zazwyczaj będziesz używać składni impl Trait, którą poznaliśmy w Rozdziale 10. Możesz zwracać dowolny typ funkcji, używając Fn, FnOnce i FnMut. Na przykład, kod z Listing 20-32 skompiluje się bez problemu.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}
Listing 20-32: Zwracanie domknięcia z funkcji przy użyciu składni impl Trait

Jednakże, jak zauważyliśmy w sekcji „Wnioskowanie i adnotacje typów domknięć” w Rozdziale 13, każde domknięcie jest również swoim własnym, odrębnym typem. Jeśli musisz pracować z wieloma funkcjami, które mają tę samą sygnaturę, ale różne implementacje, będziesz musiał użyć dla nich obiektu cechy. Rozważ, co się stanie, jeśli napiszesz kod taki jak pokazano w Listing 20-33.

Filename: src/main.rs
fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
    move |x| x + init
}
Listing 20-33: Tworzenie Vec<T> domknięć zdefiniowanych przez funkcje, które zwracają typy impl Fn

W tym miejscu mamy dwie funkcje, returns_closure i returns_initialized_closure, które obie zwracają impl Fn(i32) -> i32. Zauważ, że domknięcia, które zwracają, są różne, mimo że implementują tę samą cechę. Jeśli spróbujemy to skompilować, Rust poinformuje nas, że to nie zadziała:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
  --> src/main.rs:2:44
   |
 2 |     let handlers = vec![returns_closure(), returns_initialized_closure(123)];
   |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
 9 | fn returns_closure() -> impl Fn(i32) -> i32 {
   |                         ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
   |                                              ------------------- the found opaque type
   |
   = note: expected opaque type `impl Fn(i32) -> i32`
              found opaque type `impl Fn(i32) -> i32`
   = note: distinct uses of `impl Trait` result in different opaque types

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

Komunikat o błędzie informuje nas, że za każdym razem, gdy zwracamy impl Trait, Rust tworzy unikalny typ nieprzezroczysty – typ, w którego szczegóły tego, co Rust dla nas konstruuje, nie możemy wniknąć, ani nie możemy odgadnąć typu, który Rust wygeneruje, aby samemu go napisać. Zatem, mimo że te funkcje zwracają domknięcia, które implementują tę samą cechę, Fn(i32) -> i32, nieprzezroczyste typy, które Rust generuje dla każdego z nich, są odrębne. (Jest to podobne do tego, jak Rust produkuje różne konkretne typy dla różnych bloków async, nawet jeśli mają ten sam typ wyjściowy, jak widzieliśmy w „Typ Pin i cecha Unpin w Rozdziale 17.) Rozwiązanie tego problemu widzieliśmy już kilkakrotnie: Możemy użyć obiektu cechy, jak w Listing 20-34.

fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + init)
}
Listing 20-34: Tworzenie Vec<T> domknięć zdefiniowanych przez funkcje, które zwracają Box<dyn Fn>, tak aby miały ten sam typ

Ten kod skompiluje się bez problemu. Więcej informacji na temat obiektów cech znajdziesz w sekcji „Używanie obiektów cech do abstrakcji nad wspólnym zachowaniem” w Rozdziale 18.

Następnie przyjrzyjmy się makrom!