Продвинутые функции и замыкания
Этот раздел исследует некоторые продвинутые возможности, связанные с функциями и замыканиями, включая указатели на функции и возвращение замыканий.
Указатели на функции
Мы говорили о том, как передавать замыкания в функции; вы также можете передавать обычные функции в функции! Этот приём полезен, когда вы хотите передать уже определённую функцию вместо создания нового замыкания. Функции приводятся к типу fn (со строчной f), не путать с трейтом замыкания Fn. Тип fn называется указателем на функцию. Передача функций с помощью указателей на функции позволяет использовать функции в качестве аргументов других функций.
Синтаксис указания, что параметр является указателем на функцию, похож на синтаксис для замыканий, как показано в листинге 20-28, где мы определили функцию add_one, которая добавляет 1 к своему параметру. Функция do_twice принимает два параметра: указатель на функцию для любой функции, которая принимает параметр i32 и возвращает i32, и одно значение i32. Функция do_twice вызывает функцию f дважды, передавая ей значение arg, затем складывает результаты двух вызовов функции. Функция main вызывает do_twice с аргументами add_one и 5.
Файл: 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}"); }
Листинг 20-28: Использование типа 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 нет замыканий.
В качестве примера, где вы могли бы использовать либо замыкание, определённое inline, либо именованную функцию, рассмотрим использование метода map, предоставляемого трейтом Iterator в стандартной библиотеке. Чтобы использовать метод map для превращения вектора чисел в вектор строк, мы могли бы использовать замыкание, как в листинге 20-29.
#![allow(unused)] 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(); }
Листинг 20-29: Использование замыкания с методом map для преобразования чисел в строки
Или мы могли бы указать именованную функцию в качестве аргумента для map вместо замыкания. Листинг 20-30 показывает, как это выглядит.
#![allow(unused)] 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(); }
Листинг 20-30: Использование функции String::to_string с методом map для преобразования чисел в строки
Обратите внимание, что мы должны использовать полностью квалифицированный синтаксис, о котором мы говорили в разделе «Продвинутые трейты», потому что доступно несколько функций с именем to_string.
Здесь мы используем функцию to_string, определённую в трейте ToString, которую стандартная библиотека реализовала для любого типа, реализующего Display.
Вспомните из раздела «Значения перечислений» в главе 6, что имя каждого варианта перечисления, которое мы определяем, также становится функцией-инициализатором. Мы можем использовать эти функции-инициализаторы как указатели на функции, которые реализуют трейты замыканий, что означает, что мы можем указывать функции-инициализаторы в качестве аргументов для методов, принимающих замыкания, как показано в листинге 20-31.
#![allow(unused)] fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
Листинг 20-31: Использование инициализатора перечисления с методом 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 } }
Листинг 20-32: Возвращение замыкания из функции с использованием синтаксиса impl Trait
Однако, как мы отмечали в разделе «Выведение и аннотирование типов замыканий» в главе 13, каждое замыкание также является своим собственным уникальным типом. Если вам нужно работать с несколькими функциями, которые имеют одинаковую сигнатуру, но разные реализации, вам нужно будет использовать для них трейт-объект. Рассмотрим, что произойдёт, если вы напишете код, показанный в листинге 20-33.
Файл: 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 }
Листинг 20-33: Создание Vec
Здесь у нас есть две функции, returns_closure и returns_initialized_closure, которые обе возвращают impl Fn(i32) -> i32. Обратите внимание, что замыкания, которые они возвращают, различны, даже though они реализуют один и тот же тип. Если мы попытаемся скомпилировать это, 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` (opaque type at <src/main.rs:9:25>)
found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:46>)
= 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) — тип, в детали которого мы не можем заглянуть, и не можем угадать, какой тип сгенерирует Rust, чтобы написать его самим. Поэтому, даже though эти функции возвращают замыкания, реализующие один и тот же трейт Fn(i32) -> i32, непрозрачные типы, которые Rust генерирует для каждого, различны. (Это похоже на то, как Rust создаёт разные конкретные типы для различных async-блоков, даже когда они имеют один и тот же выходной тип, как мы видели в разделе «Тип Pin и трейт Unpin» в главе 17.) Мы уже видели решение этой проблемы несколько раз: мы можем использовать трейт-объект, как в листинге 20-34.
#![allow(unused)] fn main() { 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) } }
Листинг 20-34: Создание Vec
Этот код скомпилируется без проблем. Для получения дополнительной информации о трейт-объектах обратитесь к разделу «Использование трейт-объектов для абстрагирования общего поведения» в главе 18.
Далее давайте посмотрим на макросы!