Avatar ·

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

Привет, коллеги! Я начал изучать Rust и никак не могу разобраться с замыканиями. В документации написано, что каждое замыкание автоматически реализует один или несколько трейтов: `Fn`, `FnMut` и `FnOnce`. Но я не понимаю, от чего это зависит. Допустим, у меня есть простой код: ```rust let mut x = 5; let add_to_x = |y| x += y; // замыкание, которое мутирует x let print_x = || println!("{}", x); // замыкание, которое читает x ``` Почему `add_to_x` не может быть использовано как `Fn`, а `print_x` может? И когда замыкание становится `FnOnce`? Я пробовал передавать замыкания в функции с разными ограничениями трейтов, и компилятор ругается, что трейт не реализован. Можете на пальцах объяснить, как компилятор Rust решает, какой трейт реализовать для замыкания? И как это влияет на то, можно ли вызывать замыкание несколько раз или только один раз? Буду благодарен за примеры с разными сценариями захвата переменных. Очень хочу разобраться, потому что без этого не могу понять, как правильно проектировать API с функциями высшего порядка. <<>>

Как 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. Это даст пользователям максимальную гибкость.

<<>>

Быстрый чек-лист для определения трейта

Если вы сомневаетесь, какой трейт реализует ваше замыкание, задайте себе три вопроса:

  1. Захватывает ли замыкание значение по значению (move)? Если да → FnOnce. Пример: let c = move || { let x = vec; };
  2. Изменяет ли замыкание захваченную переменную? Если да → FnMut. Пример: let mut c = || x += 1;
  3. Только читает? → 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. Он означает "захватывай по значению", но если значение копируемое, замыкание может быть вызвано многократно.