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 (со строчной f) — не путать с трейтом замыканий Fn. Тип fn называется указателем на функцию (function pointer). Указатели на функции позволяют передавать функции аргументами в другие функции.

Синтаксис, указывающий, что параметр — указатель на функцию, похож на синтаксис замыканий, как в листинге 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 первым аргументом в do_twice можно передать просто имя функции add_one.

В отличие от замыканий, 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();
}
Листинг 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();
}
Листинг 20-30: Функция String::to_string в методе map превращает числа в строки

Обратите внимание: здесь нужен полностью квалифицированный синтаксис, о котором мы говорили в разделе «Продвинутые трейты», потому что функций с именем 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();
}
Листинг 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<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`
              found opaque type `impl Fn(i32) -> i32`
   = 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) — тип, в устройство которого мы не можем заглянуть и который не можем угадать, чтобы написать его сами. Поэтому, хотя обе функции возвращают замыкания, реализующие один и тот же трейт Fn(i32) -> i32, непрозрачные типы, которые Rust создаёт для каждой, — разные. (Это похоже на то, как Rust создаёт разные конкретные типы для разных асинхронных блоков, даже если тип результата у них одинаковый, — мы видели это в разделе «Тип Pin и трейт Unpin» главы 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)
}
Листинг 20-34: Создаём Vec<T> из замыканий, которые возвращают функции с типом Box<dyn Fn>, — так у них один и тот же тип

Этот код прекрасно скомпилируется. Подробнее о трейт-объектах — в разделе «Трейт-объекты: абстракция над общим поведением» главы 18.

Дальше посмотрим на макросы!