Продвинутые функции и замыкания

В этом разделе рассматриваются некоторые продвинутые возможности, относящиеся к функциям и замыканиям, такие как указатели функций и возвращаемые замыкания.

Указатели функций

Мы уже обсуждали, как передавать замыкания в функции; но также можно передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать ранее созданную функцию, а не определять новое замыкание. Функции соответствуют типу 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


#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-advanced-features/listing-19-27/src/main.rs}}
}

Листинг 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 для преобразования вектора чисел в вектор строк, мы можем использовать замыкание, например, так:


#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-15-map-closure/src/main.rs:here}}
}

Или мы можем использовать функцию в качестве аргумента map вместо замыкания, например, так:


#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-16-map-function/src/main.rs:here}}
}

Обратите внимание, что мы должны использовать полный синтаксис, о котором мы говорили ранее в разделе "Продвинутые трейты", потому что доступно несколько функций с именем to_string. Здесь мы используем функцию to_string определённую в типаже ToString, который реализован в стандартной библиотеке для любого типа реализующего типаж Display.

Вспомните из раздела "Значения перечислений" главы 6, что имя каждого определённого нами варианта перечисления также становится функцией-инициализатором. Мы можем использовать эти инициализаторы в качестве указателей на функции, реализующих трейты замыканий, что означает, что мы можем использовать инициализаторы в качестве аргументов для методов, принимающих замыкания, например, так:


#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-17-map-initializer/src/main.rs:here}}
}

Здесь мы создаём экземпляры Status::Value, используя каждое значение u32 в диапазоне (0..20), с которым вызывается map с помощью функции инициализатора Status::Value. Некоторые люди предпочитают этот стиль, а некоторые предпочитают использовать замыкания. Оба варианта компилируется в один и тот же код, поэтому используйте любой стиль, который вам понятнее.

Возврат замыканий

Замыкания представлены трейтами, что означает, что вы не можете возвращать замыкания из функций. В большинстве случаев, когда вам захочется вернуть трейт, вы можете использовать конкретный тип, реализующий этот трейт, в качестве возвращаемого значения функции. Однако вы не можете сделать подобного с замыканиями, поскольку у них не может быть конкретного типа, который можно было бы вернуть; например, вы не можете использовать указатель на функцию fn в качестве возвращаемого типа.

Следующий код пытается напрямую вернуть замыкание, но он не компилируется:

{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-18-returns-closure/src/lib.rs}}

Ошибка компилятора выглядит следующим образом:

{{#include ../listings/ch19-advanced-features/no-listing-18-returns-closure/output.txt}}

Ошибка снова ссылается на типаж Sized ! Rust не знает, сколько памяти нужно будет выделить для замыкания. Мы видели решение этой проблемы ранее. Мы можем использовать типаж-объект:

{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-19-returns-closure-trait-object/src/lib.rs}}

Этот код просто отлично компилируется. Для получения дополнительной информации об типаж-объектах обратитесь к разделу "Использование типаж-объектов которые допускают значения разных типов" главы 17.

Далее давайте посмотрим на макросы!