Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

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

Мы уже говорили о том, как передавать замыкания в функции; вы также можете передавать в функции обычные функции! Этот приём полезен, когда вы хотите передать уже определённую функцию, а не создавать новое замыкание. Функции приводятся к типу fn (с маленькой буквы), чтобы не путать с типажом замыкания Fn. Тип fn называется указателем на функцию. Передача функций с помощью указателей на функции позволит вам использовать функции в качестве аргументов для других функций.

Синтаксис указания того, что параметр является указателем на функцию, похож на синтаксис для замыканий, как показано в Листинге 20-28, где мы определили функцию add_one, которая добавляет 1 к своему параметру. Функция do_twice принимает два параметра: указатель на функцию любой функции, которая принимает параметр i32 и возвращает i32, а также одно значение i32. Функция do_twice вызывает функцию f дважды, передавая ей значение arg, а затем складывает результаты двух вызовов. Функция main вызывает do_twice с аргументами add_one и 5.

Filename: 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}");
}
Listing 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 нет замыканий.

В качестве примера, где можно использовать либо замыкание, определённое встроенным способом, либо именованную функцию, рассмотрим использование метода 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();
}
Listing 20-29: Использование замыкания с методом 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();
}
Listing 20-30: Использование метода String::to_string для преобразования чисел в строки

Обратите внимание, что мы должны использовать полностью квалифицированный синтаксис, о котором мы говорили в разделе «Продвинутые типажи», потому что существует несколько функций с именем 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();
}
Listing 20-31: Использование инициализатора перечисления с методом map для создания экземпляра Status из чисел

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

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

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

Вместо этого вы обычно будете использовать синтаксис impl Trait, который мы изучили в главе 10. Вы можете вернуть любой тип функции, используя Fn, FnOnce и FnMut. Например, код в Листинге 20-32 будет работать perfectly.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}
Listing 20-32: Возврат замыкания из функции с использованием синтаксиса impl Trait

Однако, как мы отметили в разделе «Вывод типов замыканий и аннотации» в главе 13, каждое замыкание также является собственным отдельным типом. Если вам нужно работать с несколькими функциями, имеющими одинаковую сигнатуру, но разные реализации, вам придётся использовать объект типажа для них. Посмотрите, что произойдёт, если вы напишете код, подобный показанному в Листинге 20-33.

Filename: 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
}
Listing 20-33: Создание 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` (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 создаёт уникальный непрозрачный тип — тип, в детали которого мы не можем заглянуть, который Rust конструирует для нас. Поэтому, даже though эти функции обе возвращают замыкания, реализующие один и тот же типаж, Fn(i32) -> i32, непрозрачные типы, которые Rust генерирует для каждого из них, различны. (Это похоже на то, как Rust производит разные конкретные типы для отдельных асинхронных блоков, даже когда у них одинаковый тип возвращаемого значения, как мы видели в разделе «Работа с любым количеством фьючерсов» в главе 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)
}
Listing 20-34: Создание Vec<T> из замыканий, определённых функциями, которые возвращают Box<dyn Fn>, чтобы они имели один и тот же тип

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

Далее рассмотрим макросы!