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));}Вывод:
4065Читай F: Fn(u32) -> u32 так: «F — какой-то тип, который можно вызвать с u32, и он вернёт u32». Подходят оба замыкания, хотя их типы разные.
Три трейта: Fn, FnMut и FnOnce
Заголовок раздела «Три трейта: Fn, FnMut и FnOnce»Компилятор смотрит, что тело замыкания делает с захваченными значениями:
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]));}Вывод:
6070[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 call8 | 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
Заголовок раздела «Возвращаем замыкания через impl Fn»Функция может и собрать замыкание, и вернуть его. Тип результата записывается как 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)));}Вывод:
252135Зачем 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 и носит её с собой — как если бы ты взял обед с собой, а не оставил его в чужом холодильнике.
Замыкания в Box: много замыканий, один тип
Заголовок раздела «Замыкания в Box: много замыканий, один тип»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(i32) -> i32
Заголовок раздела «Указатели на функции: fn(i32) -> i32»Обычные функции тоже можно передавать. Их тип пишется с маленькой 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}");}Вывод:
8337У всех указателей на функции один тип, поэтому их можно сложить в массив без всяких коробок. Подвох в том, что замыкание, которое что-то захватывает, указателем на функцию стать не может. Компилятор скажет: 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 this4 | | } else {5 | | move |total| total + fee | | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure6 | | } | |_____- `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 objecthelp: 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 нет: это поле, а не метод. Чтобы вызвать то, что в нём хранится, возьми обращение к полю в скобки.
Практика
Заголовок раздела «Практика»1. Автомат с талонами
Заголовок раздела «1. Автомат с талонами»Напиши функцию 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.
2. Помощник для повторных попыток
Заголовок раздела «2. Помощник для повторных попыток»Напиши 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("данные загружены"), вызовов: 3retry вызывает задачу больше одного раза, поэтому 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
Что почитать
Заголовок раздела «Что почитать»- Перемещение захваченных значений из замыканий и трейты Fn — книга «Язык программирования Rust»: официальное объяснение того, как компилятор выбирает
Fn,FnMutилиFnOnce(перевод на русский). Fn,FnMutиFnOnce— документация стандартной библиотеки (на английском): определение каждого трейта с короткими примерами, когда его использовать.- Как входные параметры — Rust на примерах: запускаемые примеры функций, которые принимают замыкания с каждым из трёх ограничений (перевод на русский).
- Как выходные параметры — Rust на примерах: возврат замыканий через
impl Fnи зачем нуженmove(перевод на русский). - Closure types — The Rust Reference (на английском): точные правила того, как замыкания захватывают значения и какие трейты реализуют.