Когда замыкание реализует трейты Fn, FnMut и FnOnce в Rust?
Как Rust выбирает трейт для замыкания?
Всё зависит от того, как замыкание взаимодействует с захваченными переменными. Компилятор автоматически реализует максимально "слабый" трейт, который всё ещё позволяет замыканию работать. Вот три правила:
- FnOnce — если замыкание перемещает захваченное значение внутрь себя (например, передаёт его в другую функцию или возвращает). Такое замыкание можно вызвать только один раз.
- FnMut — если замыкание мутирует захваченную переменную (через
&mut) или перемещает ссылку на неё. Можно вызывать много раз, но только если замыкание объявлено какmut. - Fn — если замыкание только читает захваченные переменные (через
&) или не захватывает ничего. Можно вызывать сколько угодно раз, даже безmut.
Важный нюанс: каждое замыкание, реализующее Fn, также реализует FnMut и FnOnce. Аналогично, FnMut автоматически реализует FnOnce. Это иерархия наследования трейтов.
Пример 1: Fn (только чтение)
let x = 42;
let reader = || println!("x = {}", x); // захват по &
fn call_fn(f: impl Fn()) {
f();
}
call_fn(reader); // ✅ работает
call_fn(reader); // ✅ можно вызвать ещё раз
Пример 2: FnMut (мутация)
let mut counter = 0;
let mut increment = || counter += 1; // захват по &mut
fn call_fn_mut(f: impl FnMut()) {
f();
}
call_fn_mut(&mut increment); // ✅ нужно передавать мутабельную ссылку
call_fn_mut(&mut increment); // ✅ можно вызвать ещё раз
Обратите внимание: замыкание объявлено как let mut, и мы передаём &mut в функцию. Без этого компилятор не даст мутировать захваченную переменную.
Пример 3: FnOnce (перемещение)
let s = String::from("hello");
let consumer = || drop(s); // s перемещается внутрь замыкания
fn call_fn_once(f: impl FnOnce()) {
f();
}
call_fn_once(consumer); // ✅ один раз
// call_fn_once(consumer); // ❌ ошибка: consumer уже перемещён
Здесь drop(s) забирает владение s. После первого вызова s уничтожено, поэтому замыкание больше нельзя вызвать.
Как проверить, какой трейт реализован?
Можно явно указать тип замыкания и посмотреть ошибку компилятора:
let x = 5;
let closure = || x + 1;
let _: impl Fn() = closure; // ✅
let _: impl FnMut() = closure; // ✅ (Fn автоматически реализует FnMut)
let _: impl FnOnce() = closure; // ✅ (Fn автоматически реализует FnOnce)
А для мутирующего замыкания:
let mut y = 10;
let mut closure = || y += 1;
let _: impl FnMut() = closure; // ✅
// let _: impl Fn() = closure; // ❌ ошибка: не реализует Fn
Практический совет
При проектировании API используйте наиболее общий трейт, который вам нужен. Если функция будет вызывать замыкание один раз — используйте FnOnce. Если несколько раз без мутации — Fn. Если с мутацией — FnMut. Это даст пользователям максимальную гибкость.
Быстрый чек-лист для определения трейта
Если вы сомневаетесь, какой трейт реализует ваше замыкание, задайте себе три вопроса:
- Захватывает ли замыкание значение по значению (move)? Если да →
FnOnce. Пример:let c = move || { let x = vec; }; - Изменяет ли замыкание захваченную переменную? Если да →
FnMut. Пример:let mut c = || x += 1; - Только читает? →
Fn. Пример:let c = || println!("{}", x);
Ещё один полезный трюк: если вы хотите передать замыкание, которое захватывает по значению, но при этом не перемещает переменную (например, просто копирует число), компилятор может реализовать Fn вместо FnOnce. Rust оптимизирует: если тип захваченной переменной реализует Copy, замыкание может быть Fn, даже если вы написали move.
let x = 42;
let c = move || x + 1; // x копируется, т.к. i32: Copy
// c всё ещё Fn, а не FnOnce!
fn call_fn(f: impl Fn() -> i32) {
println!("{}", f());
}
call_fn(c);
call_fn(c); // ✅ работает дважды
Это важно помнить: move не означает автоматически FnOnce. Он означает "захватывай по значению", но если значение копируемое, замыкание может быть вызвано многократно.
Ответов пока нет.
Войдите чтобы оставить ответ