Продвинутые функции и замыкания
В этом разделе разбираются продвинутые возможности, связанные с функциями и замыканиями, — указатели на функции и возврат замыканий.
Указатели на функции
Мы уже говорили, как передавать в функции замыкания; но в функции можно
передавать и обычные функции! Это удобно, когда нужно передать уже
определённую функцию, а не создавать новое замыкание. Функции приводятся к типу
fn (со строчной f) — не путать с трейтом замыканий Fn. Тип fn
называется указателем на функцию (function pointer). Указатели на функции
позволяют передавать функции аргументами в другие функции.
Синтаксис, указывающий, что параметр — указатель на функцию, похож на синтаксис
замыканий, как в листинге 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}");
}
fn позволяет принять указатель на функцию в качестве аргументаЭтот код выводит The answer is: 12. Мы указываем, что параметр f в
do_twice — это fn, принимающий один параметр типа i32 и возвращающий i32.
Затем f можно вызвать в теле do_twice. А в main первым аргументом в
do_twice можно передать просто имя функции add_one.
В отличие от замыканий, fn — это тип, а не трейт, поэтому тип параметра
указывается прямо как fn, а не через обобщённый параметр типа с ограничением
одним из трейтов Fn.
Указатели на функции реализуют все три трейта замыканий (Fn, FnMut и
FnOnce), так что указатель на функцию всегда можно передать в функцию,
которая ожидает замыкание. Функции лучше писать с обобщённым типом и одним из
трейтов замыканий — тогда они будут принимать и функции, и замыкания.
Тем не менее бывает, что нужно принимать только fn, а не замыкания, — например,
при взаимодействии с внешним кодом, где замыканий нет: функции C могут
принимать функции аргументами, но замыканий в C нет.
В качестве примера, где можно использовать и замыкание, определённое прямо на
месте, и именованную функцию, посмотрим на метод map трейта Iterator из
стандартной библиотеки. Чтобы с помощью map превратить вектор чисел в вектор
строк, можно использовать замыкание, как в листинге 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 превращает числа в строкиА можно вместо замыкания передать в map имя функции. Как это выглядит —
показано в листинге 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();
}
String::to_string в методе map превращает числа в строкиОбратите внимание: здесь нужен полностью квалифицированный синтаксис, о
котором мы говорили в разделе «Продвинутые трейты», потому что функций с именем to_string несколько.
Здесь используется функция to_string из трейта ToString, который стандартная
библиотека реализовала для любого типа, реализующего Display.
Вспомните из раздела «Значения перечисления» главы 6: имя каждого варианта перечисления становится ещё и функцией- инициализатором. Эти функции-инициализаторы можно использовать как указатели на функции, реализующие трейты замыканий, а значит, передавать их аргументами в методы, принимающие замыкания, как в листинге 20-31.
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
map создаёт экземпляры Status из чиселЗдесь мы создаём экземпляры Status::Value из каждого значения u32 в
диапазоне, у которого вызван map, — с помощью функции-инициализатора
Status::Value. Одним нравится такой стиль, другим — замыкания. Компилируются
они в одинаковый код, так что выбирайте тот стиль, который вам понятнее.
Возврат замыканий
Замыкания представлены трейтами, поэтому вернуть замыкание напрямую нельзя.
Обычно, когда хочется вернуть трейт, можно вернуть из функции конкретный тип,
реализующий этот трейт. Но с замыканиями так, как правило, не получится: у них
нет конкретного типа, который можно было бы вернуть. Например, если замыкание
захватывает какие-то значения из своей области видимости, вернуть его как
указатель на функцию fn нельзя.
Вместо этого обычно используют синтаксис impl Trait из главы 10. Вернуть
можно функцию любого вида — через Fn, FnOnce и FnMut. Например, код из
листинга 20-32 прекрасно скомпилируется.
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
impl TraitОднако, как мы отмечали в разделе «Выведение и аннотации типов замыканий» главы 13, у каждого замыкания ещё и свой отдельный тип. Если нужно работать с несколькими функциями с одинаковой сигнатурой, но разной реализацией, для них придётся использовать трейт-объект. Посмотрите, что будет, если написать код вроде листинга 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> из замыканий, которые возвращают функции с типом impl FnЗдесь две функции, 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 создаёт уникальный непрозрачный тип (opaque type) — тип, в устройство
которого мы не можем заглянуть и который не можем угадать, чтобы написать его
сами. Поэтому, хотя обе функции возвращают замыкания, реализующие один и тот же
трейт Fn(i32) -> i32, непрозрачные типы, которые Rust создаёт для каждой, —
разные. (Это похоже на то, как Rust создаёт разные конкретные типы для разных
асинхронных блоков, даже если тип результата у них одинаковый, — мы видели это
в разделе «Тип Pin и трейт Unpin» главы 17.)
Решение мы уже видели несколько раз — трейт-объект, как в листинге 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> из замыканий, которые возвращают функции с типом Box<dyn Fn>, — так у них один и тот же типЭтот код прекрасно скомпилируется. Подробнее о трейт-объектах — в разделе «Трейт-объекты: абстракция над общим поведением» главы 18.
Дальше посмотрим на макросы!