Поглиблено про функції та замикання

Цей підрозділ поглиблено досліджує функціонал, що стосується функцій та замикань, і включає з вказівники на функції та повернення замикань.

Вказівники на функції

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

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

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 у діапазоні, для якого викликається mao, використовуючи функцію ініціалізації 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 {
  |                         ~~~~~~~~~~~~~~~~~~~

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

Помилка знову посилається на трейт Sized! Іржа не знає, скільки місця потрібно для зберігання замикання. Ви вже бачили розв'язок цієї проблеми. Ми можемо скористатися трейтовим об'єктом:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

Цей код чудово компілюється. Щоб дізнатися більше про трейтові об'єкти, зверніться до підрозділу "Використання трейтових об'єктів, що можуть бути значеннями різних типів" з Розділу 17.

Далі розгляньмо макроси! ch19-03-advanced-traits.html#advanced-traits ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types