Когда замыкание реализует Fn, FnMut и FnOnce в Rust?
Краткий ответ: всё зависит от того, как замыкание использует захваченные переменные
Компилятор Rust автоматически определяет, какой из трёх типажей реализует замыкание, на основе того, что оно делает с захваченными переменными из внешней области видимости. Вот основное правило:
- FnOnce — замыкание потребляет захваченное значение (например, перемещает его). Такое замыкание можно вызвать только один раз.
- FnMut — замыкание изменяет захваченное значение (через &mut ссылку). Его можно вызывать несколько раз, но с мутабельным доступом.
- Fn — замыкание только читает захваченное значение (через & ссылку) или не захватывает ничего. Его можно вызывать сколько угодно раз, даже одновременно из нескольких потоков.
Разбираем ваш пример
let x = 10;
let print_x = || println!("{}", x); // Fn
Замыкание print_x только читает x (выводит на экран). Оно захватывает x по неизменяемой ссылке &i32. Поскольку i32 реализует Copy, компилятор мог бы даже скопировать значение, но в данном случае он берёт ссылку. Результат — Fn.
let mut y = String::from("hello");
let append = || y.push_str(" world"); // FnMut
Метод push_str требует &mut self, поэтому замыкание захватывает y по мутабельной ссылке &mut String. Это делает его FnMut. Вы можете вызывать его несколько раз, но только последовательно (без одновременных вызовов).
let z = String::from("rust");
let consume = || drop(z); // FnOnce
Функция drop принимает значение по владению. Поэтому замыкание перемещает z внутрь себя. После первого вызова z будет уничтожен, и замыкание больше не сможет быть вызвано — это FnOnce.
Как это влияет на вызовы?
Любое замыкание, реализующее Fn, автоматически реализует и FnMut, и FnOnce. Аналогично, FnMut реализует FnOnce. Но не наоборот: FnOnce не может быть использован там, где требуется Fn.
fn call_fn(f: impl Fn()) { f(); }
fn call_fn_mut(mut f: impl FnMut()) { f(); f(); }
fn call_fn_once(f: impl FnOnce()) { f(); }
let x = 10;
let print = || println!("{}", x);
call_fn(&print); // OK: Fn
call_fn_mut(&mut print); // OK: FnMut (т.к. Fn реализует FnMut)
call_fn_once(print); // OK: FnOnce
let mut y = String::from("hi");
let append = || y.push_str("!");
call_fn_mut(&mut append); // OK: FnMut
call_fn_once(append); // OK: FnOnce
// call_fn(&append) — ОШИБКА: FnMut не реализует Fn
let z = String::from("bye");
let consume = || drop(z);
call_fn_once(consume); // OK: FnOnce
// call_fn_mut(&mut consume) — ОШИБКА: FnOnce не реализует FnMut
// call_fn(&consume) — ОШИБКА
Как понять, какой типаж ожидает функция?
Смотрите на сигнатуру:
impl Fn()— функция может вызывать замыкание несколько раз, без ограничений.impl FnMut()— функция может вызывать замыкание несколько раз, но с мутабельным доступом (например, в циклеfor).impl FnOnce()— функция вызовет замыкание ровно один раз (например, вthread::spawnилиOption::map).
Если сомневаетесь, используйте самый общий типаж FnOnce — он примет любое замыкание. Но если ваша функция вызывает замыкание несколько раз, придётся использовать Fn или FnMut.
Важное замечание про move
Ключевое слово move перед замыканием заставляет его захватывать переменные по значению (перемещать их). Это часто используется для передачи замыканий в потоки. move не меняет типаж: если замыкание только читает Copy-тип, оно всё равно может быть Fn, даже с move. Но если оно перемещает String — будет FnOnce.
Практический совет: используйте трейты по убыванию строгости
Когда вы пишете функцию, которая принимает замыкание, старайтесь указывать наименее строгий типаж, который вам нужен. Это даст максимальную гибкость вызывающему коду.
// Плохо: слишком строго
fn process(f: impl Fn() -> i32) { /* ... */ }
// Хорошо: если замыкание вызывается один раз
fn process_once(f: impl FnOnce() -> i32) -> i32 { f() }
// Ещё лучше: если замыкание вызывается многократно
fn process_mut(f: impl FnMut() -> i32) { /* ... */ }
Обратите внимание на стандартную библиотеку:
Iterator::mapпринимаетFnMut— потому что итератор может вызывать замыкание много раз, но не требует неизменяемости.thread::spawnпринимаетFnOnce + Send + 'static— потому что поток может выполнить замыкание только один раз.Option::and_thenпринимаетFnOnce— замыкание вызывается не более одного раза.
Если вы не уверены, какой типаж использовать, начните с FnOnce. Если компилятор пожалуется, что нужно вызывать замыкание несколько раз, переключитесь на FnMut или Fn. Компилятор подскажет вам правильный путь!
Ответов пока нет.
Войдите чтобы оставить ответ