Замыкания в Rust: магия или строгая математика?
Если вы когда-нибудь писали на Rust что-то вроде |x| x + 1, вы использовали замыкания. Но как только вы пытаетесь передать это замыкание в функцию или сохранить в структуру, компилятор вдруг начинает требовать: «А какой трейт? Fn, FnMut или FnOnce?».
И тут многие впадают в ступор. Особенно если вы пришли из JavaScript, Python или Ruby, где замыкания — это «просто функции, которые захватывают переменные». В Rust всё сложнее (и интереснее).
Сегодня разберёмся, что на самом деле происходит под капотом, когда Rust решает, какой трейт дать вашему замыканию. Это не магия — это строгая система типов, которая защищает вас от гонок данных и висячих указателей. Поехали!
Коротко о главном: три трейта замыканий
В Rust есть три трейта, которые реализуют замыкания:
FnOnce— может быть вызвано один раз. Забирает владение захваченными переменными.FnMut— может быть вызвано многократно, но может изменять захваченные переменные (берёт&mut).Fn— может быть вызвано многократно и не изменяет захваченные переменные (берёт&).
Каждое замыкание автоматически реализует все подходящие трейты. То есть если замыкание реализует Fn, оно автоматически реализует и FnMut, и FnOnce. Но наоборот — не работает.
Почему? Потому что Fn — это самый строгий, а FnOnce — самый слабый. Иерархия наследования выглядит так:
FnOnce ← FnMut ← Fn
То есть FnMut: FnOnce, а Fn: FnMut. Запомните эту лесенку — она ключевая.
Как Rust решает, какой трейт дать замыканию?
Компилятор смотрит на то, как именно замыкание использует захваченные переменные. Есть три режима захвата:
- По ссылке (
&T) — замыкание только читает переменную. - По изменяемой ссылке (
&mut T) — замыкание изменяет переменную. - По значению (
T) — замыкание забирает владение переменной.
Вот как это влияет на трейты:
| Способ захвата | Реализуемые трейты | Пример |
|---|---|---|
Только &T | Fn + FnMut + FnOnce | || println!("{}", x) |
&mut T (но не T) | FnMut + FnOnce | || x += 1 |
T (владение) | Только FnOnce | || drop(x) или move || x |
Давайте разберём каждый случай с живым кодом.
Пример 1: Только чтение → Fn
let x = 42;
let read_closure = || println!("x = {}", x);
// Можно вызывать сколько угодно раз
read_closure();
read_closure();
// И даже передать в функцию, требующую Fn
fn call_fn<F: Fn()>(f: F) {
f();
}
call_fn(read_closure);
Здесь замыкание просто читает x. Ему не нужно его менять или забирать. Rust понимает: «Ок, это безопасно — даём Fn».
Пример 2: Изменение → FnMut
let mut x = 42;
let mut mut_closure = || {
x += 1;
println!("x = {}", x);
};
mut_closure(); // x = 43
mut_closure(); // x = 44
// Передаём в функцию, требующую FnMut
fn call_fn_mut<F: FnMut()>(mut f: F) {
f();
f();
}
call_fn_mut(mut_closure);
Обратите внимание: мы объявили mut_closure как let mut. Почему? Потому что вызов FnMut требует изменяемой ссылки на само замыкание (оно же хранит &mut x внутри).
Важно: Если попробовать передать это замыкание в функцию, которая ждёт Fn, компилятор выдаст ошибку. Замыкание, которое изменяет захваченные данные, не может быть Fn — оно нарушило бы правило «никаких изменений».
Пример 3: Захват владения → FnOnce
let s = String::from("hello");
let once_closure = || {
drop(s); // забираем владение!
println!("s is dropped");
};
// Работает — один раз
once_closure();
// А вот второй раз уже не получится:
// once_closure(); // ОШИБКА: use of moved value: `once_closure`
Почему так? Потому что drop(s) перемещает s внутрь замыкания. После первого вызова s уничтожено, и замыкание больше не может быть вызвано — оно потеряло свои данные.
Компилятор видит: «Замыкание забирает владение → только FnOnce».
Ключевой нюанс: ключевое слово move
Иногда вы хотите принудительно передать владение переменной в замыкание. Для этого есть ключевое слово move:
let x = vec![1, 2, 3];
let closure = move || {
println!("{:?}", x); // x теперь принадлежит замыканию
};
// println!("{:?}", x); // ОШИБКА: x moved
closure();
closure(); // А вот это — сюрприз! Работает!
Парадокс? Замыкание с move может быть вызвано несколько раз? Да, если оно не уничтожает захваченные данные. move просто меняет способ захвата (по владению вместо ссылки), но не определяет, какой трейт будет реализован.