Розширені функції та замикання
Цей розділ досліджує деякі розширені можливості, пов’язані з функціями та замиканнями, зокрема вказівники на функції та повернення замикань.
Вказівники на функції
Ми вже говорили про те, як передавати замикання до функцій; ви також можете передавати звичайні
функції до функцій! Ця техніка корисна, коли ви хочете передати
функцію, яку ви вже визначили, замість визначення нового замикання. Функції
приводяться до типу fn (з малою літерою f), не слід плутати з
трейтoм Fn для замикань. Тип fn називається вказівником на функцію. Передавання
функцій за допомогою вказівників на функції дасть змогу використовувати функції як аргументи
інших функцій.
Синтаксис для вказання того, що параметр є вказівником на функцію, подібний до
синтаксису замикань, як показано в Listing 20-28, де ми визначили функцію
add_one, яка додає 1 до свого параметра. Функція do_twice приймає два
параметри: вказівник на функцію для будь-якої функції, яка приймає параметр i32
і повертає i32, та одне значення i32. Функція do_twice викликає
функцію f двічі, передаючи їй значення arg, а потім додає два результати
виклику функції разом. Функція main викликає do_twice з аргументами
add_one і 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}");
}
Цей код друкує The answer is: 12. Ми вказуємо, що параметр f у
do_twice є fn, який приймає один параметр типу i32 і повертає
i32. Потім ми можемо викликати f у тілі do_twice. У main ми можемо передати
ім’я функції add_one як перший аргумент до do_twice.
На відміну від замикань, fn є типом, а не трейтом, тож ми вказуємо fn як
тип параметра безпосередньо, а не оголошуємо узагальнений параметр типу з одним
із трейтів Fn як обмеження трейтів.
Вказівники на функції реалізують усі три трейти замикань (Fn, FnMut і
FnOnce), тобто ви завжди можете передати вказівник на функцію як аргумент для
функції, яка очікує замикання. Найкраще писати функції, використовуючи узагальнений
тип і один із трейтів Fn, щоб ваші функції могли приймати або
функції, або замикання.
Тим не менш, один приклад, де ви б хотіли приймати лише fn, а не
замикання, — це взаємодія із зовнішнім кодом, який не має замикань: функції C
можуть приймати функції як аргументи, але C не має замикань.
Як приклад того, де ви могли б використати або замикання, визначене безпосередньо,
або іменовану функцію, подивімося на використання методу map, наданого
трейтoм Iterator у стандартній бібліотеці. Щоб використати метод map для перетворення вектора
чисел у вектор рядків, ми могли б використати замикання, як у 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 замість замикання.
Listing 20-30 показує, як це виглядало б.
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();
}
Зауважте, що ми повинні використовувати повністю кваліфікований синтаксис, про який ми говорили в
розділі “Advanced Traits”, тому що існує
кілька доступних функцій з назвою to_string.
Тут ми використовуємо функцію to_string, визначену в трейтi ToString,
який стандартна бібліотека реалізувала для будь-якого типу, що реалізує
Display.
Пригадайте з розділу “Enum Values” у Chapter 6, що ім’я кожного варіанта переліку, який ми визначаємо, також стає функцією ініціалізації. Ми можемо використовувати ці функції ініціалізації як вказівники на функції, що реалізують трейти замикань, що означає, що ми можемо вказувати функції ініціалізації як аргументи для методів, які приймають замикання, як видно в Listing 20-31.
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
Тут ми створюємо екземпляри Status::Value, використовуючи кожне значення u32 у діапазоні,
на якому викликається map, за допомогою функції ініціалізації Status::Value.
Деякі люди надають перевагу цьому стилю, а деякі люди надають перевагу використанню замикань. Вони
компілюються в той самий код, тож використовуйте той стиль, який вам зрозуміліший.
Повернення замикань
Замикання представлені трейтами, що означає, що ви не можете повертати замикання
безпосередньо. У більшості випадків, коли ви могли б захотіти повернути трейт, ви можете натомість
використати конкретний тип, який реалізує трейт, як значення, що повертається
функцією. Однак зазвичай ви не можете зробити це із замиканнями, тому що вони не
мають конкретного типу, який можна повернути; вам не дозволено використовувати вказівник на функцію fn
як тип, що повертається, якщо замикання захоплює будь-які значення зі своєї
області видимості, наприклад.
Натомість ви зазвичай використовуватимете синтаксис impl Trait, який ми вивчили в
Chapter 10. Ви можете повертати будь-який тип функції, використовуючи Fn, FnOnce і FnMut.
Наприклад, код у Listing 20-32 буде чудово компілюватися.
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
Однак, як ми зазначали в розділі “Inferring and Annotating Closure Types” у Chapter 13, кожне замикання також є своїм окремим типом. Якщо вам потрібно працювати з кількома функціями, що мають однаковий сигнатуру, але різні реалізації, вам потрібно буде використовувати трейт-об’єкт для них. Подивіться, що станеться, якщо ви напишете код, подібний до того, що показано в 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
}
Тут у нас є дві функції, returns_closure і returns_initialized_closure,
які обидві повертають impl Fn(i32) -> i32. Зверніть увагу, що замикання, які вони
повертають, різні, навіть якщо вони реалізують один і той самий тип. Якщо ми спробуємо
скомпілювати це, Rust повідомить нам, що це не спрацює:
$ 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
Повідомлення про помилку каже нам, що щоразу, коли ми повертаємо impl Trait, Rust
створює унікальний непрозорий тип, тип, всередину деталей якого ми не можемо
зазирнути, які Rust конструює для нас, і ми також не можемо вгадати тип, який Rust згенерує,
щоб написати його самостійно. Отже, хоча ці функції повертають замикання, що реалізують
один і той самий трейт, Fn(i32) -> i32, непрозорі типи, які Rust генерує для кожного з них, є
різними. (Це схоже на те, як Rust створює різні конкретні типи
для різних async-блоків, навіть коли вони мають той самий тип виводу, як ми бачили в
“The Pin Type and the Unpin Trait” у
Chapter 17.) Ми вже бачили рішення цієї проблеми кілька разів: ми можемо
використати трейт-об’єкт, як у 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)
}
Цей код чудово скомпілюється. Докладніше про трейт-об’єкти дивіться в розділі “Using Trait Objects To Abstract over Shared Behavior” у Chapter 18.
Далі подивімося на макроси!