Avatar ·

Когда замыкание реализует Fn, FnMut и FnOnce в Rust?

Я новичок в Rust и пытаюсь разобраться с замыканиями. Прочитал про типажи Fn, FnMut и FnOnce, но не до конца понимаю, когда компилятор выбирает какой из них. Написал небольшой пример: ```rust fn main() { let x = 10; let print_x = || println!("{}", x); // Замыкание 1 let mut y = String::from("hello"); let append = || y.push_str(" world"); // Замыкание 2 let z = String::from("rust"); let consume = || drop(z); // Замыкание 3 } ``` Почему первое замыкание работает как Fn, второе как FnMut, а третье как FnOnce? И как это влияет на то, сколько раз я могу вызвать замыкание? Если я передам такое замыкание в функцию, как узнать, какой типаж она ожидает? Буду благодарен за объяснение с примерами. <<>>

Краткий ответ: всё зависит от того, как замыкание использует захваченные переменные

Компилятор 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. Компилятор подскажет вам правильный путь!