Расширенные возможности функций и замыканий
В этом разделе рассматриваются некоторые расширенные возможности, связанные с функциями и замыканиями, включая указатели на функции и возврат замыканий.
Указатели на функции
Мы говорили о том, как передавать замыкания в функции; вы также можете
передавать обычные функции в функции! Эта техника полезна, когда вы хотите
передать уже определенную функцию, а не определять новое замыкание. Функции
приводятся к типу fn (со строчной f), который не следует путать с трейтом
замыкания Fn. Тип fn называется указателем на функцию. Передача функций
с помощью указателей на функции позволит вам использовать функции как
аргументы для других функций.
Синтаксис указания того, что параметр является указателем на функцию, похож на
синтаксис замыканий, как показано в листинге 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 мы можем передать
имя функции add_one как первый аргумент для do_twice.
В отличие от замыканий, 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 создает уникальный непрозрачный тип — тип, в детали которого мы
не можем заглянуть, и мы не можем угадать тип, который Rust сгенерирует для
нас, чтобы записать его самостоятельно. Поэтому, хотя эти функции возвращают
замыкания, реализующие один и тот же трейт, Fn(i32) -> i32, непрозрачные типы,
которые Rust генерирует для каждой из них, различны. (Это похоже на то, как
Rust создает разные конкретные типы для разных async-блоков, даже когда у них
один и тот же выходной тип, как мы видели в разделе «Тип 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.
Далее давайте посмотрим на макросы!