способ захвата окружения в системе типовthe capture mode encoded in the type system

Замыкания: Fn / FnMut / FnOnceClosures: Fn / FnMut / FnOnce

Замыкание — анонимная функция, способная захватывать переменные из окружающей области видимости. Каждое замыкание автоматически реализует один, два или все три трейта — Fn, FnMut, FnOnce — в зависимости от того, что именно тело замыкания делает со своими захватами; программист это не объявляет вручную.A closure is an anonymous function that can capture variables from its enclosing scope. Every closure automatically implements one, two, or all three of the traits Fn, FnMut, FnOnce, depending on what its body actually does with its captures — the programmer doesn't declare this explicitly.

основа языкаcore of the language частый вопрос: в чём разница Fn/FnMut/FnOnce и когда какой нуженcommon question: the difference between Fn/FnMut/FnOnce and when each is needed

Что этоWhat it is

Замыкание записывается как |параметры| тело (например, |x, y| x + y, или move |x| x + captured_var). Каждое замыкание реализует один, два или все три трейта: Fn (можно вызывать многократно, нужен только &self — захватывает по ссылке и не изменяет захваченное), FnMut (можно вызывать многократно, нужен &mut self — захватывает по изменяемой ссылке либо мутирует окружение), FnOnce (можно вызвать только один раз, нужен self по значению — захватывает/потребляет переменную, которую не может вернуть обратно). Какой именно трейт(ы) реализует конкретное замыкание, компилятор выводит автоматически из того, что тело замыкания реально делает со своими захватами.A closure is written |params| body (e.g. |x, y| x + y, or move |x| x + captured_var). Every closure implements one, two, or all three traits: Fn (callable multiple times, only needs &self — captures by reference and doesn't mutate them), FnMut (callable multiple times, needs &mut self — captures by mutable reference or mutates its environment), FnOnce (callable only once, needs self by value — captures/consumes a variable it can't give back). Which trait(s) a given closure implements is inferred automatically by the compiler from what the closure's body actually does with its captures.

let numbers = vec![1, 2, 3, 4, 5];
let threshold = 2;

let above: Vec<&i32> = numbers.iter()
    .filter(|&&x| x > threshold)  // Fn: читает threshold по ссылке, вызывается на каждый элемент// Fn: reads threshold by reference, called once per element
    .collect();

Зачем ввелиWhy it was introduced

Три трейта кодируют ownership/borrowing напрямую в типе замыкания:The three traits encode ownership/borrowing directly in the closure's own type:

01

Контракт захвата — часть типа, а не рантайм-сюрпризThe capture contract is part of the type, not a runtime surprise

Функция, принимающая F: FnOnce(), явно говорит вызывающей стороне: "я вызову это замыкание ровно один раз, поэтому тебе можно передать мне замыкание, потребляющее/перемещающее что-то из захваченного, что оно не сможет вернуть обратно". F: Fn() обещает обратное: "я могу вызвать это сколько угодно раз, или сохранить и вызывать позже многократно, поэтому не давай мне то, что можно запустить лишь однажды". Это напрямую опирается на ownership.html и borrowing-references.html.A function accepting F: FnOnce() is explicitly telling callers: "I will call this closure exactly once, so you may pass me a closure that consumes/moves something it captured that it can't give back". F: Fn() promises the opposite: "I may call this any number of times, or store it and call it later repeatedly, so don't give me something that can only run once." This directly builds on ownership.html and borrowing-references.html.

02

Точнее, чем std::function в C++More precise than C++'s std::function

C++'s std::function полностью стирает это различие — std::function, обёртывающий move-only захват, требует обходных путей, потому что operator() у std::function спроектирован как const/копируемо-ориентированный. Система трейтов Rust делает контракт режима захвата частью самой системы типов, а не рантайм-сюрпризом или ограничением класса-обёртки.C++'s std::function erases this distinction entirely — a std::function wrapping a move-only capture requires workarounds, since std::function's call operator is const/copyable-oriented by design. Rust's trait-based closures make the capture-mode contract part of the type system itself, not a runtime surprise or a wrapper-type limitation.

03

Наименее ограничивающий трейт выводится автоматическиThe least restrictive trait is inferred automatically

Поскольку компилятор сам определяет, какой трейт(ы) нужен, программист не должен заранее декларировать намерение — достаточно написать тело замыкания, и система типов сама отразит его фактическое поведение по отношению к захваченным переменным.Since the compiler determines which trait(s) apply, the programmer doesn't have to declare intent up front — writing the closure's body is enough, and the type system reflects its actual behavior toward captured variables automatically.

Как это работает под капотомHow it works under the hood

Замыкание, мутирующее захваченный счётчик, реализует FnMut. Замыкание с move, передаваемое в thread::spawn, забирает владение захваченным значением — spawn требует как минимум FnOnce (гарантированно вызывается ровно один раз, поэтому более сильный Fn/FnMut не нужен).A closure mutating a captured counter implements FnMut. A move closure passed to thread::spawn takes ownership of the captured value — spawn's actual bound is FnOnce (it's guaranteed to run exactly once, so the stronger Fn/FnMut isn't required).

let mut count = 0;
let mut increment = || count += 1;  // FnMut: мутирует захваченный count// FnMut: mutates the captured count
increment();
increment();
println!("{}", count);  // 2// 2

let data = vec![1, 2, 3];
let handle = std::thread::spawn(move || {
    println!("{:?}", data);  // FnOnce: spawn требует не больше FnOnce, потому что вызов гарантированно один// FnOnce: spawn requires no more than FnOnce, since the call is guaranteed to run once
});
handle.join().unwrap();

Функция, принимающая замыкание, объявляет требуемый трейт явно в границах дженерика: fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32.A function accepting a closure declares the required trait explicitly as a generic bound: fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32.

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Fn — переиспользуемое замыкание в конвейере map/filterFn — a reusable closure in a map/filter pipeline

Замыкание, захватывающее пороговое значение по ссылке и вызываемое многократно на каждый элемент, — классический Fn: оно ничего не меняет в окружении и может вызываться сколько угодно раз.A closure capturing a threshold by reference and called repeatedly per element is a classic Fn: it mutates nothing in its environment and can be called any number of times.

let prices = vec![9.99, 15.5, 3.0, 42.0];
let min_price = 10.0;

let expensive: Vec<f64> = prices.iter()
    .filter(|&&p| p >= min_price)  // Fn: читает min_price, вызывается для каждого элемента// Fn: reads min_price, called for every element
    .copied()
    .collect();
02

FnMut — накопление бегущей суммы через callbackFnMut — accumulating a running total via a callback

Замыкание, вызываемое один раз на каждый элемент потока и мутирующее захваченную переменную-накопитель между вызовами, обязано быть FnMut — оно не готово стать простым Fn, потому что каждый вызов зависит от состояния, оставленного предыдущим.A closure invoked once per element of a stream, mutating a captured accumulator variable between calls, must be FnMut — it can't be a plain Fn because each call depends on state left by the previous one.

let readings = vec![10, 20, 15];
let mut running_sum = 0;

readings.iter().for_each(|&r| {
    running_sum += r;  // FnMut: мутирует running_sum на каждый вызов// FnMut: mutates running_sum on every call
    println!("running total: {}", running_sum);
});
03

FnOnce/move — поток, потребляющий данные ровно один разFnOnce/move — a thread consuming data exactly once

Замыкание с move, передающее захваченные данные в новый поток, забирает владение ими — после этого замыкание нельзя вызвать снова: захваченные данные уже потреблены при первом (и единственном) вызове.A move closure handing captured data to a new thread takes ownership of it — the closure can't be called again afterward: the captured data was already consumed by the first (and only) call.

let payload = String::from("job-42");

let handle = std::thread::spawn(move || {
    process(payload);  // payload перемещён и потреблён внутри — FnOnce// payload is moved in and consumed — FnOnce
});
handle.join().unwrap();
// println!("{}", payload); // ОШИБКА: payload уже перемещён в замыкание// println!("{}", payload); // ERROR: payload was already moved into the closure
04

Vec<Box<dyn Fn(i32) -> i32>> как реестр операцийVec<Box<dyn Fn(i32) -> i32>> as a pluggable-operations registry

Разные замыкания имеют разные анонимные типы, поэтому хранить их вместе в коллекции можно только стерев конкретный тип — Box<dyn Fn(i32) -> i32> здесь и есть trait object (см. trait-objects.html), просто в форме замыкания.Different closures have different anonymous types, so storing them together in a collection requires erasing the concrete type — Box<dyn Fn(i32) -> i32> here is exactly a trait object (see trait-objects.html), just in closure form.

let ops: Vec<Box<dyn Fn(i32) -> i32>> = vec![
    Box::new(|x| x + 1),
    Box::new(|x| x * 2),
    Box::new(|x| x * x),
];

for op in &ops {
    println!("{}", op(5));  // 6, 10, 25 — вызов через vtable// 6, 10, 25 — dispatched through a vtable
}

ГраблиGotchas

move добавляют "чтобы скомпилировалось", не всегда понимая зачемmove gets added "to make it compile" without always understanding why

Компилятор выводит наименее ограничивающий режим захвата, который реально требуется телу замыкания — замыкание, только читающее захваченную переменную, будет Fn, даже если вызвать его планируется всего раз, а замыкание без move по умолчанию захватывает по ссылке, что иногда вызывает проблемы с временем жизни, когда замыкание должно пережить своё окружение (например, возвращается из функции или перемещается в порождённый поток) — добавление move как способ "заставить скомпилироваться" — частый шаг, который новички делают почти вслепую, не до конца понимая, зачем он потребовался.The compiler infers the least restrictive capture mode a closure's body actually requires — a closure that merely reads a captured variable is Fn even if you don't intend to call it more than once, and a closure without move borrows by reference by default, which occasionally causes lifetime issues when the closure needs to outlive its environment (e.g. being returned from a function or moved into a spawned thread) — adding move as a "make it compile" step is common, and beginners often apply it somewhat blindly without fully understanding why it was needed.

Fn ⊂ FnMut ⊂ FnOnce — просите наименее ограничивающий трейтFn ⊂ FnMut ⊂ FnOnce — ask for the least restrictive trait

Три трейта образуют иерархию по тому, что может принять вызывающая сторона: любое Fn-замыкание одновременно годится и как FnMut, и как FnOnce (более способное замыкание тривиально удовлетворяет менее требовательной границе), но не наоборот — поэтому параметр функции, ограниченный наиболее permissive трейтом, который реально нужен (обычно FnOnce, если вызов только один раз), принимает максимально широкий диапазон замыканий от вызывающей стороны. Излишне строгая граница (требовать Fn, когда достаточно FnOnce) без нужды отвергает вполне валидные замыкания.The three traits form a hierarchy in terms of what a caller can accept: every Fn closure is also a valid FnMut and FnOnce (a strictly more capable closure trivially satisfies a less demanding bound), but not the reverse — so a function parameter bounded by the most permissive trait it actually needs (usually FnOnce if calling only once) accepts the widest range of caller closures. Bounding overly strictly (requiring Fn when FnOnce would suffice) needlessly rejects perfectly valid caller closures.

Возврат замыкания требует impl Fn(...) или Box<dyn Fn(...)>Returning a closure requires impl Fn(...) or Box<dyn Fn(...)>

Возврат замыкания из функции требует либо impl Fn(...) -> ... (непрозрачный тип возврата, один конкретный тип замыкания, проще), либо Box<dyn Fn(...) -> ...> (стёртый тип, позволяет возвращать разные конкретные типы замыкания из разных веток, с небольшой ценой vtable/аллокации) — написать напрямую анонимное, сгенерированное компилятором имя типа замыкания как тип возврата нельзя, что удивляет новичков, пришедших из языков, где "просто верни лямбду" не требует такого различения.Returning a closure from a function requires either impl Fn(...) -> ... (opaque return type, one concrete closure type, simpler) or Box<dyn Fn(...) -> ...> (type-erased, allows returning different concrete closure types from different branches, at a small vtable/allocation cost) — you cannot write the closure's own anonymous compiler-generated type name directly as a return type, which surprises newcomers coming from languages where "just return the lambda" needs no such distinction.