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

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

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

Мы говорили о том, как передавать замыкания в функции; но вы также можете передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать функцию, которую вы уже определили, а не объявлять новое замыкание. Указатель функции позволит использовать функции как аргументы к другим функциям. Функции приводятся (coerce) к типу fn (с нижним регистром f), не к путать с типажом замыкания Fn. Тип fn называется указателем функции. Синтаксис для указания того, что параметр является указателем функции, похож на замыкание как показано в листинге 19-27.

Файл: 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 для принятия указателя функции в качестве аргумента

Этот код печатает 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. Для использования функции 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.

У нас есть ещё один полезный шаблон, который использует детали реализации структур кортежей (tuple structs) и вариантов перечислений структур кортежей (tuple-struct enum). Эти типы используют () в качестве синтаксиса инициализатора, который выглядит как вызов функции. Инициализаторы на самом деле реализованы как функции, возвращающие экземпляр, который построен из их аргументов. Мы можем использовать эти функции инициализаторы как указатели на функции, которые реализуют типажи замыканий, что означает мы можем указать инициализирующие функции в качестве аргументов для методов, которые принимают замыкания, например:

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:14]`, which implements `Fn(i32) -> i32`
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example`

To learn more, run the command again with --verbose.

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


#![allow(unused)]
fn main() {
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
}

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

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