Продвинутые функции и замыкания
В этом разделе рассматриваются некоторые продвинутые возможности, относящиеся к функциям и замыканиям, такие как указатели функций и возвращаемые замыкания.
Указатели функций
Мы уже обсуждали, как передавать замыкания в функции; но также можно передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать ранее созданную функцию, а не определять новое замыкание. Функции соответствуют типу fn
(со строчной буквой f), не путать с трейтом замыкания Fn
. Тип fn
называется указателем функции. Передача функций с помощью указателей функций позволяет использовать функции в качестве аргументов других функций.
Для указания того, что параметр является указателем на функцию, используется синтаксис, такой же, как и для замыканий, что демонстрируется в листинге 19-27, где мы определили функцию add_one
, которая добавляет единицу к переданному ей параметру. Функция 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); }
Листинг 19-27: Использование типа fn
для получения указателя на функцию в качестве аргумента
Этот код выводит Ответ: 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
для преобразования вектора чисел в вектор строк, мы можем использовать замыкание, например, так:
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
вместо замыкания, например, так:
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(); }
Обратите внимание, что мы должны использовать полный синтаксис, о котором мы говорили ранее в разделе "Продвинутые трейты", потому что доступно несколько функций с именем to_string
. Здесь мы используем функцию to_string
определённую в типаже ToString
, который реализован в стандартной библиотеке для любого типа реализующего типаж Display
.
Вспомните из раздела "Значения перечислений" главы 6, что имя каждого определённого нами варианта перечисления также становится функцией-инициализатором. Мы можем использовать эти инициализаторы в качестве указателей на функции, реализующих трейты замыканий, что означает, что мы можем использовать инициализаторы в качестве аргументов для методов, принимающих замыкания, например, так:
fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
Здесь мы создаём экземпляры Status::Value
, используя каждое значение u32
в диапазоне (0..20), с которым вызывается map
с помощью функции инициализатора Status::Value
. Некоторые люди предпочитают этот стиль, а некоторые предпочитают использовать замыкания. Оба варианта компилируется в один и тот же код, поэтому используйте любой стиль, который вам понятнее.
Возврат замыканий
Замыкания представлены трейтами, что означает, что вы не можете возвращать замыкания из функций. В большинстве случаев, когда вам захочется вернуть трейт, вы можете использовать конкретный тип, реализующий этот трейт, в качестве возвращаемого значения функции. Однако вы не можете сделать подобного с замыканиями, поскольку у них не может быть конкретного типа, который можно было бы вернуть; например, вы не можете использовать указатель на функцию fn
в качестве возвращаемого типа.
Следующий код пытается напрямую вернуть замыкание, но он не компилируется:
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
Ошибка компилятора выглядит следующим образом:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
--> src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ~~~~~~~~~~~~~~~~~~~
For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` due to previous error
Ошибка снова ссылается на типаж Sized
! Rust не знает, сколько памяти нужно будет выделить для замыкания. Мы видели решение этой проблемы ранее. Мы можем использовать типаж-объект:
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
Этот код просто отлично компилируется. Для получения дополнительной информации об типаж-объектах обратитесь к разделу "Использование типаж-объектов которые допускают значения разных типов" главы 17.
Далее давайте посмотрим на макросы!