Перейти к содержимому

Fn, FnMut и FnOnce в Rust

В уроке Замыкания ты научился писать замыкание, узнал, как оно захватывает переменные и как может их менять. Эта страница отвечает на следующий вопрос: как написать свою функцию, которая принимает замыкание? И как вернуть замыкание из функции?

Ответ — три небольших трейта: Fn, FnMut и FnOnce. Представь правила в библиотеке. Одни посетители только читают книги (Fn). Другие пишут заметки на полях (FnMut). А третьи уносят книгу домой и больше не возвращают (FnOnce). Rust смотрит, что делает твоё замыкание, и вешает на него подходящую табличку. Если поначалу трейты кажутся страшными, не переживай: их всего три, и компилятор сам выбирает нужный.

У каждого замыкания свой уникальный тип, который придумывает компилятор. Написать имя этого типа нельзя. Даже у двух замыканий, которые выглядят совершенно одинаково, типы разные:

fn main() {
let mut rules = vec![|price: u32| price * 2];
rules.push(|price: u32| price * 2);
}
error[E0308]: mismatched types
--> src/main.rs:3:16
|
3 | rules.push(|price: u32| price * 2);
| ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
|
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object

Компилятор говорит: ожидалось одно замыкание, а найдено другое, и даже у двух одинаковых замыканий типы не совпадают. Поэтому функция не может сказать «дай мне замыкание типа X». Вместо этого она говорит «дай мне любой тип, который можно вызвать вот так». Это ограничение трейта (trait bound) — такое же, как в уроках Обобщения и Трейты:

fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 {
rule(price)
}
fn main() {
let coupon = 15;
let half_price = |price: u32| price / 2;
let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80));
println!("{}", price_after(use_coupon, 80));
}

Вывод:

40
65
▶ Попробуй сам

Читай F: Fn(u32) -> u32 так: «F — какой-то тип, который можно вызвать с u32, и он вернёт u32». Подходят оба замыкания, хотя их типы разные.

Компилятор смотрит, что тело замыкания делает с захваченными значениями:

  • Fn: только читает их. Вызывать можно сколько угодно раз.
  • FnMut: меняет их. Вызывать можно много раз, но нужен mut.
  • FnOnce: отдаёт их (перемещает наружу). Вызвать можно только один раз, потому что после этого значения уже нет.
fn main() {
let shop = String::from("Пекарня на углу");
// Fn: только читает `shop`
let greet = || println!("Добро пожаловать в «{shop}»!");
greet();
greet();
// FnMut: меняет `orders`
let mut orders = 0;
let mut take_order = || {
orders += 1;
println!("заказ №{orders} принят");
};
take_order();
take_order();
// FnOnce: отдаёт `receipt` вызывающему коду
let receipt = String::from("2 круассана, 1 кофе");
let hand_over = || receipt;
let customer_copy = hand_over();
println!("у покупателя: {customer_copy}");
}

Вывод:

Добро пожаловать в «Пекарня на углу»!
Добро пожаловать в «Пекарня на углу»!
заказ №1 принят
заказ №2 принят
у покупателя: 2 круассана, 1 кофе
▶ Попробуй сам

hand_over возвращает сам receipt, а не копию. Когда чек отдан, замыканию больше нечего отдавать. Поэтому оно FnOnce.

Вот картинка, которую стоит держать в голове. Представь три круга, один внутри другого:

  • Большой внешний круг — это FnOnce. Любое замыкание можно вызвать хотя бы раз, поэтому здесь все замыкания.
  • Внутри него круг поменьше — FnMut: замыкания, которые можно вызывать снова и снова.
  • В самом центре — Fn: замыкания, которые можно вызывать снова и снова, ничего не меняя.

Значит, каждое Fn-замыкание — это ещё и FnMut, и FnOnce. Это важно, когда ты пишешь функцию. Параметр, который просит FnOnce, принимает любое замыкание. А параметр, который просит Fn, — самый привередливый.

Одна вещь часто удивляет: ключевое слово move трейт не выбирает. move || println!("{shop}") забирает владение shop, но только читает её, поэтому остаётся Fn. Решает только тело.

Одно и то же ограничение можно записать тремя способами. Смысл у них одинаковый, так что выбирай тот, что читается лучше:

// 1. Обобщённый параметр с ограничением
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 2. `impl Fn` на месте аргумента: короче, имя не нужно
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// 3. Блок `where`: аккуратно, когда ограничение длинное
fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
scores.iter().map(|&s| f(s)).collect()
}
fn main() {
let bonus = 10;
let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50));
println!("{}", apply_twice(add_bonus, 50));
println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));
}

Вывод:

60
70
[50, 75, 100]
▶ Попробуй сам

Ты можешь удивиться, как получилось передать add_bonus три раза. Замыкание, которое только читает захваченное, дёшево скопировать, поэтому Rust каждый раз его копирует.

Теперь FnMut и FnOnce. С FnMut параметру нужен mut, потому что вызов замыкания меняет то, что у него внутри. FnOnce можно вызвать один раз — и всё:

fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
for task in tasks {
on_done(task);
}
}
fn on_shutdown<F: FnOnce()>(cleanup: F) {
println!("завершаю работу...");
cleanup();
}
fn main() {
let mut finished = Vec::new();
process_tasks(&["разослать письма", "сделать бэкап базы"], |task| {
finished.push(task.to_string());
});
println!("готово: {:?}", finished);
on_shutdown(|| {
// Этот цикл забирает владение `finished`, поэтому замыкание — FnOnce
for task in finished {
println!("в архиве: {task}");
}
});
}

Вывод:

готово: ["разослать письма", "сделать бэкап базы"]
завершаю работу...
в архиве: разослать письма
в архиве: сделать бэкап базы
▶ Попробуй сам

Хорошее правило: проси самый свободный трейт, с которым твоя функция может жить. Если ты вызываешь замыкание только один раз, проси FnOnce — тогда передать можно что угодно. Так сделано и в стандартной библиотеке: Option::map принимает FnOnce, а Iterator::map — FnMut.

Компилятор ловит каждое несовпадение, и сообщения у него здесь очень дружелюбные. Допустим, функция просит Fn, а твоё замыкание меняет счётчик:

fn run_report<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let mut runs = 0;
run_report(|| {
runs += 1;
println!("отчёт, запуск {runs}");
});
}
error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure
--> src/main.rs:9:9
|
1 | fn run_report<F: Fn()>(f: F) {
| - change this to accept `FnMut` instead of `Fn`
...
8 | run_report(|| {
| ---------- -- in this closure
| |
| expects `Fn` instead of `FnMut`
9 | runs += 1;
| ^^^^^^^^^ cannot assign

Компилятор сообщает, что нельзя присвоить значение runs, потому что это захваченная переменная в Fn-замыкании, — и даже подсказывает исправление: принимать FnMut и добавить параметру mut. После этого программа выведет отчёт, запуск 1 и отчёт, запуск 2:

fn run_report<F: FnMut()>(mut f: F) {
f();
f();
}

Другая классическая ошибка — вызвать FnOnce-замыкание дважды:

fn main() {
let report = String::from("продажи за третий квартал");
let send = || {
let sent = report;
println!("отправляю: {sent}");
};
send();
send();
}
error[E0382]: use of moved value: `send`
--> src/main.rs:8:5
|
7 | send();
| ------ `send` moved due to this call
8 | send();
| ^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `report` out of its environment
--> src/main.rs:4:20
|
4 | let sent = report;
| ^^^^^^

Примечание (note) говорит прямо: замыкание нельзя вызвать больше одного раза, потому что оно перемещает report из своего окружения. Первый вызов отдал report, и для второго ничего не осталось. Если два вызова действительно нужны, заимствуй (let sent = &report;), и замыкание станет Fn. Если передать такое замыкание в функцию, которая просит Fn, получишь похожее сообщение: error[E0507]: cannot move out of report, a captured variable in an Fn closure.

Функция может и собрать замыкание, и вернуть его. Тип результата записывается как impl Fn(i32) -> i32, то есть «какой-то тип замыкания; знать его имя не нужно»:

fn make_adder(amount: i32) -> impl Fn(i32) -> i32 {
move |x| x + amount
}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let add_tip = make_adder(5);
let triple = make_multiplier(3);
println!("{}", add_tip(20));
println!("{}", triple(7));
println!("{}", add_tip(triple(10)));
}

Вывод:

25
21
35
▶ Попробуй сам

Зачем move? Без него замыкание только заимствовало бы amount. Но amount принадлежит make_adder и исчезает в тот момент, когда make_adder возвращает результат. Замыкание держало бы ссылку в пустоту. Rust отказывается:

error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |x| x + amount
| ^^^ ------ `amount` is borrowed here
| |
| may outlive borrowed value `amount`
|
help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x + amount
| ++++

Ошибка говорит: замыкание может пережить текущую функцию, но заимствует amount, которой эта функция владеет; чтобы замыкание забрало владение, добавь move. С move замыкание упаковывает свою копию amount и носит её с собой — как если бы ты взял обед с собой, а не оставил его в чужом холодильнике.

impl Fn обозначает один скрытый тип. Поэтому нельзя вернуть одно замыкание из ветки if, а другое — из ветки else. И нельзя положить разные замыкания в один Vec. Для обеих задач положи каждое замыкание в коробку: Box<dyn Fn(f64) -> f64> значит «любое замыкание такой формы, хранящееся в куче». Это та же идея, что и в уроке Трейт-объекты.

Вот касса, которая применяет список именованных ценовых правил по порядку:

fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
if express {
Box::new(move |total| total + fee * 2.0)
} else {
Box::new(move |total| total + fee)
}
}
fn main() {
let member_percent = 10.0;
let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![
("скидки участника", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))),
("купона", Box::new(move |total: f64| total - coupon)),
("обычной доставки", shipping_rule(false, 3.5)),
];
let mut total = 80.0;
println!("начало: {total:.2}");
for (name, rule) in &rules {
total = rule(total);
println!("после {name}: {total:.2}");
}
}

Вывод:

начало: 80.00
после скидки участника: 72.00
после купона: 67.00
после обычной доставки: 70.50
▶ Попробуй сам

Коробка стоит крошечного выделения памяти в куче, а каждый вызов идёт через указатель. Почти никогда это не проблема. Используй impl Fn, когда тип замыкания один, и Box<dyn Fn>, когда их много.

Структура тоже может хранить замыкание. Раз тип замыкания назвать нельзя, структуру делают обобщённой по нему. Вот маленькая кнопка, которая выполняет код при нажатии:

struct Button<F: Fn()> {
label: String,
on_click: F,
}
impl<F: Fn()> Button<F> {
fn click(&self) {
println!("[{}] нажата", self.label);
(self.on_click)();
}
}
fn main() {
let user = String::from("Ада");
let save = Button {
label: String::from("Сохранить"),
on_click: move || println!("сохраняю черновик: {user}"),
};
save.click();
save.click();
}

Вывод:

[Сохранить] нажата
сохраняю черновик: Ада
[Сохранить] нажата
сохраняю черновик: Ада
▶ Попробуй сам

Обрати внимание на скобки в (self.on_click)(). Они говорят Rust: «возьми поле, а потом вызови его». Если кнопке нужно при каждом нажатии менять захваченное, используй F: FnMut() и сделай так, чтобы метод принимал &mut self.

Обычные функции тоже можно передавать. Их тип пишется с маленькой fn, например fn(i32) -> i32. Это указатель на функцию (function pointer) — просто адрес кода. Замыкание, которое ничего не захватывает, тоже может в него превратиться:

fn double(x: i32) -> i32 {
x * 2
}
fn square(x: i32) -> i32 {
x * x
}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 {
op(x)
}
fn main() {
println!("{}", apply_op(double, 4));
println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1];
let mut value = 3;
for step in steps {
value = step(value);
}
println!("{value}");
}

Вывод:

8
3
37
▶ Попробуй сам

У всех указателей на функции один тип, поэтому их можно сложить в массив без всяких коробок. Подвох в том, что замыкание, которое что-то захватывает, указателем на функцию стать не может. Компилятор скажет: closures can only be coerced to fn types if they do not capture any variables («замыкания приводятся к типам fn, только если ничего не захватывают»). Зато в обратную сторону всё хорошо: каждая обычная функция реализует Fn, FnMut и FnOnce, так что double можно передать в apply из примера выше. Поэтому в большинстве кода используют трейты замыканий: они принимают и то и другое.

Забыть mut у параметра FnMut. Вызов FnMut его меняет, поэтому привязка должна быть mut:

error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable
--> src/main.rs:3:9
|
3 | on_done(task);
| ^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
| +++

Компилятор сообщает, что on_done нельзя заимствовать изменяемо, потому что она не объявлена как mut, и показывает, куда добавить mut.

Вернуть разные замыкания из -> impl Fn. Два замыкания — это два типа, даже если они похожи:

fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
if express {
move |total| total + fee * 2.0
} else {
move |total| total + fee
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if express {
3 | | move |total| total + fee * 2.0
| | ------------------------------
| | |
| | the expected closure
| | expected because of this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected closure `{closure@src/main.rs:3:9: 3:21}`
found closure `{closure@src/main.rs:5:9: 5:21}`
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object
help: you could change the return type to be a boxed trait object
|
1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new(move |total| total + fee * 2.0)
4 | } else {
5 ~ Box::new(move |total| total + fee)
|

Ветки if и else дают несовместимые типы. Сделай так, как советует компилятор: верни Box<dyn Fn(f64) -> f64> и оберни каждое замыкание в Box::new(...).

Вызвать поле-замыкание как метод. Если написать self.on_click(), Rust будет искать метод с именем on_click:

error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope
--> src/main.rs:9:14
|
9 | self.on_click();
| ^^^^^^^^ field, not a method
|
help: to call the type parameter stored in `on_click`, surround the field access with parentheses
|
9 | (self.on_click)();
| + +

Метода on_click нет: это поле, а не метод. Чтобы вызвать то, что в нём хранится, возьми обращение к полю в скобки.

Напиши функцию ticket_dispenser(start: u32), которая возвращает замыкание. Каждый вызов замыкания выдаёт следующий номер талона: start, потом start + 1 и так далее. Сделай два автомата (пекарня начинает с 100, аптека — с 1) и покажи, что они считают независимо. Подсказка: замыкание меняет свой собственный счётчик, поэтому тип результата — impl FnMut() -> u32, и понадобится move.

Показать решение
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 {
let mut next = start;
move || {
let ticket = next;
next += 1;
ticket
}
}
fn main() {
let mut bakery = ticket_dispenser(100);
let mut pharmacy = ticket_dispenser(1);
println!("пекарня: {}", bakery());
println!("пекарня: {}", bakery());
println!("аптека: {}", pharmacy());
println!("пекарня: {}", bakery());
}

Вывод:

пекарня: 100
пекарня: 101
аптека: 1
пекарня: 102
▶ Попробуй сам

Каждый автомат переместил к себе свою next, поэтому счётчик у них никогда не общий. Переменным bakery и pharmacy нужен mut, потому что они FnMut.

Напиши retry(max_attempts, task). Она вызывает task, пока та не вернёт Ok, или пока не будет сделано max_attempts попыток. Для каждой неудачной попытки выводи строку. Проверь её на замыкании, которое дважды падает с ошибкой "сервер занят", а потом срабатывает. Подсказка: тестовое замыкание считает свои вызовы, поэтому ограничение — FnMut() -> Result<String, String>, и параметру нужен mut.

Показать решение
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>
where
F: FnMut() -> Result<String, String>,
{
for attempt in 1..=max_attempts {
match task() {
Ok(value) => return Ok(value),
Err(e) => println!("попытка {attempt} не удалась: {e}"),
}
}
Err(String::from("сдаюсь"))
}
fn main() {
let mut calls = 0;
let result = retry(5, || {
calls += 1;
if calls < 3 {
Err(String::from("сервер занят"))
} else {
Ok(String::from("данные загружены"))
}
});
println!("{:?}, вызовов: {calls}", result);
}

Вывод:

попытка 1 не удалась: сервер занят
попытка 2 не удалась: сервер занят
Ok("данные загружены"), вызовов: 3
▶ Попробуй сам

retry вызывает задачу больше одного раза, поэтому FnOnce не подходит. А задача меняет calls, поэтому не подходит и Fn. FnMut — ровно то, что нужно. Когда retry возвращает результат, замыкание отработало, и main снова может читать calls.

  • У каждого замыкания свой уникальный тип, поэтому функции принимают замыкания через ограничение трейта вроде F: Fn(i32) -> i32.
  • Fn только читает захваченное, FnMut меняет его, а FnOnce перемещает его наружу и вызывается один раз. Решает тело, а не move.
  • Каждое Fn — это ещё и FnMut, и FnOnce. Проси самый свободный трейт, который нужен твоей функции.
  • Ограничения пишутся как <F: Fn(..)>, impl Fn(..) или в блоке where. Параметрам FnMut нужен mut.
  • Одно замыкание возвращают через -> impl Fn(..) (обычно с move). Для разных замыканий из веток или в Vec используй Box<dyn Fn(..)>.
  • fn(i32) -> i32 — указатель на функцию. Ему подходят обычные функции и замыкания, которые ничего не захватывают.

Проверь себя

1. Замыкание делает total += price с захваченной переменной. Какой самый строгий трейт оно реализует?

2. Твоя функция вызывает параметр-замыкание ровно один раз. Какое ограничение примет больше всего замыканий?

3. Зачем make_adder(amount: i32) -> impl Fn(i32) -> i32 нужен move |x| x + amount?

4. Тебе нужен Vec с несколькими разными замыканиями-скидками. Какой тип нужен элементам?

Результат: 0 / 4