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.
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}");
}
fn do przyjęcia wskaźnika na funkcję jako argumentuTen 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();
}
map do konwersji liczb na ciągi znakówAlbo 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();
}
String::to_string z metodą map do konwersji liczb na ciągi znakówZauważ, ż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();
}
map do tworzenia instancji Status z liczbTutaj 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
}
}
impl TraitJednakż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.
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
}
Vec<T> domknięć zdefiniowanych przez funkcje, które zwracają typy impl FnW 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)
}
Vec<T> domknięć zdefiniowanych przez funkcje, które zwracają Box<dyn Fn>, tak aby miały ten sam typTen 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!