RustMiddleTechnical
Что такое closures в Rust и чем отличаются Fn, FnMut и FnOnce?
Замыкание в Rust захватывает окружение и реализует один из трёх трейтов: FnOnce (захват по значению, можно вызвать раз), FnMut (захват по &mut, мутирует окружение), Fn (захват по &, можно вызывать многократно из нескольких потоков).
Замыкания в Rust
Замыкание — анонимная функция, способная захватывать переменные из окружающего контекста. Тип каждого замыкания уникален и генерируется компилятором; взаимодействие с ними происходит через трейты Fn, FnMut, FnOnce.
Три трейта и их иерархия
FnOnce— самый слабый контракт: замыкание можно вызвать хотя бы один раз. Захватывает значения по владению (move). После вызова значение перемещено и второй вызов невозможен.FnMut: extendsFnOnce— можно вызвать несколько раз, мутируя захваченные переменные.Fn: extendsFnMut— можно вызвать несколько раз без мутации, в том числе из нескольких потоков одновременно.
Иерархия: Fn ⊂ FnMut ⊂ FnOnce. Принимая impl FnOnce, вы принимаете любое замыкание.
fn apply_once<F: FnOnce() -> String>(f: F) -> String {
f() // f перемещается, второй вызов невозможен
}
fn apply_mut<F: FnMut() -> i32>(mut f: F) -> Vec<i32> {
vec![f(), f(), f()]
}
fn apply<F: Fn(i32) -> i32>(f: F, values: &[i32]) -> Vec<i32> {
values.iter().map(|&x| f(x)).collect()
}
fn main() {
// FnOnce: захватывает String по владению
let name = String::from("Alice");
let greet = move || format!("Hello, {}!", name);
println!("{}", apply_once(greet));
// println!("{}", name); // ошибка: name перемещена
// FnMut: мутирует захваченный счётчик
let mut count = 0;
let counter = || { count += 1; count };
let results = apply_mut(counter);
println!("{:?}", results); // [1, 2, 3]
// Fn: захватывает по &
let multiplier = 3;
let triple = |x| x * multiplier;
let nums = apply(triple, &[1, 2, 3, 4]);
println!("{:?}", nums); // [3, 6, 9, 12]
}
move-замыкания и потоки
Для передачи замыкания в поток используйте move, чтобы перенести владение в замыкание — иначе лайфтаймы захваченных ссылок не будут удовлетворять 'static.
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", data); // data перемещена в поток
});
handle.join().unwrap();
impl Fn vs dyn Fn
// impl Fn — статическая диспетчеризация, zero-cost
fn double_all(items: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
items.iter().map(|&x| f(x)).collect()
}
// dyn Fn — динамическая диспетчеризация, нужна при хранении в структурах
struct Callback {
handler: Box<dyn Fn(String)>,
}
Подводные камни
- Замыкание, которое перемещает захваченное значение (даже без
move), автоматически реализует толькоFnOnce— это часто удивляет. FnMut-замыкание нельзя вызвать через&self-метод — нужен&mut selfили именованная переменная сmut.- Анонимный тип замыкания нельзя написать явно — используйте
impl Fn,Box<dyn Fn>или обобщённый параметр. dyn Fnтребует Boxing и добавляет косвенность; при горячих путях предпочитайте статическую диспетчеризацию.- Замыкание, захватившее
MutexGuard, может удерживать блокировку дольше ожидаемого, если не дать guard'у выйти из области видимости. - В async-контексте замыкание должно возвращать
async { ... }или явныйFuture—async || {}— нестабильная фича в стабильном Rust.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.