# Замыкания в Rust: функции с собственной памятью

> Простой разбор указателей на функции и замыканий в Rust. Узнаем, как замыкания захватывают окружение, в чем разница между Fn, FnMut и FnOnce и как они устроены под капотом.

- **URL:** https://eagle2000.space/rust/rust-closures/
- **Date:** 2026-08-12

- **Tags:** rust, dev, closures, advanced

---## Введение

Замыкания (или лямбда-выражения) — это анонимные функции, которые мы можем создавать прямо на лету и передавать в другие функции. Но их главная суперсила — умение **запоминать контекст**, то есть захватывать переменные из той области видимости, где они были объявлены. 

В языках программирования с автоматической сборкой мусора (GC) разработчики даже не задумываются о том, как замыкания хранят эти переменные. Но в Rust, где памятью управляет компилятор, правила работы с замыканиями подчиняются строгой логике владения и заимствования.

В этой статье мы разберем, как устроены замыкания в Rust, чем они отличаются от обычных функций, как делятся на трейты `Fn`, `FnMut` и `FnOnce` и как компилятор собирает их «под капотом».

---

## 1. Указатели на функции (`fn`)

Прежде чем говорить о замыканиях, давайте вспомним про обычные функции. 

В Rust имя функции может выступать в роли указателя на нее. Тип такого указателя пишется строчными буквами: например, `fn(i32) -> i32`.

* **Как это устроено в памяти:**
  * Указатель `fn` — это **«тонкий» указатель**. Он хранит только один адрес — адрес первой инструкции скомпилированного кода в памяти.
  * Такие функции **не имеют состояния**. Они не могут заглянуть во внешние переменные и прочитать что-то, чего им не передали в аргументах.

* **Передача указателя в качестве аргумента:**
  Мы можем передавать указатели на функции в другие функции. Например, напишем свой аналог метода `map` для слайсов:

  ```rust
  fn map<T, U>(slice: &[T], func: fn(T) -> U) -> Vec<U>
  where 
      T: Copy 
  {
      let mut result = Vec::with_capacity(slice.len());
      for value in slice {
          // value имеет тип &T. Чтобы передать его в func по значению T, 
          // нам нужно его разыменовать (*value). Это сработает, если T реализует Copy.
          result.push(func(*value)); 
      }
      result
  }
  ```

---

## 2. Замыкания: функции с рюкзаком за спиной

Замыкание — это анонимная функция, у которой за спиной есть невидимый «рюкзак» (контекст), куда она может складывать внешние переменные.

```rust
let coefficient = 10;
let multiply = |x| x * coefficient; // Замыкание захватило coefficient!
```

Аргументы замыкания пишутся внутри двух вертикальных черт `|...|`, а тело — следом. Фигурные скобки нужны только для многострочного кода.

### Вывод типов

Компилятор Rust сам понимает, какие типы данных принимает и возвращает замыкание, анализируя место его первого вызова. Но при желании вы можете прописать типы вручную:

```rust
let multiply = |x: i32| -> i32 { x * coefficient };
```

### Захват окружения и слово `move`

* **По умолчанию (по ссылке):** Замыкание пытается быть максимально экономным. Оно не копирует переменные, а берет их по ссылке. Компилятор сам решает, какая ссылка нужна: обычная для чтения (`&T`) или мутабельная для записи (`&mut T`).
* **Принудительный перенос (`move`):** Если замыкание должно жить дольше, чем окружающий код (например, мы возвращаем его из функции или передаем в другой поток), нам нужно перенести владение переменными внутрь замыкания. Для этого перед чертами пишется слово `move`:
  ```rust
  let print_data = move || println!("Данные: {}", data);
  ```

---

## 3. Три трейта замыканий: Fn, FnMut и FnOnce

В зависимости от того, что именно замыкание делает с переменными в своем «рюкзаке», оно автоматически реализует один из трех трейтов:

<div style="background-color:white; padding: 20px">
{{< mermaid >}}
graph TD
    FnOnce --> FnMut
    FnMut --> Fn
    
    style FnOnce fill:#f9f,stroke:#333,stroke-width:2px,color:#000
    style FnMut fill:#ccf,stroke:#333,stroke-width:1px,color:#000
    style Fn fill:#cfc,stroke:#333,stroke-width:1px,color:#000
{{< /mermaid >}}
</div>

1. **`FnOnce` (Поглотитель)**
   * **Что делает:** Забирает захваченные переменные по значению и расходует их (уничтожает или передает дальше).
   * **Ограничение:** Поскольку данные расходуются при первом вызове, вызвать такое замыкание можно **только один раз**.
2. **`FnMut` (Мутатор)**
   * **Что делает:** Изменяет захваченные переменные (использует эксклюзивную ссылку `&mut self`).
   * **Ограничение:** Можно вызывать сколько угодно раз, но только последовательно (нельзя запустить одновременно из разных потоков без синхронизации).
3. **`Fn` (Читатель)**
   * **Что делает:** Только читает данные (использует разделяемую ссылку `&self`).
   * **Ограничение:** Никаких ограничений! Можно вызывать многократно и параллельно из разных потоков.

### Иерархия трейтов

Они образуют матрешку:
* Любое замыкание типа `Fn` автоматически умеет работать как `FnMut` и `FnOnce`.
* Обычные указатели на функции (`fn`) не имеют состояния, поэтому всегда реализуют самый верхний трейт `Fn`.

---

## 4. Как замыкания работают в коде

### Передача в функции

Поскольку точный тип замыкания анонимен, мы используем дженерики или `impl Trait`:

```rust
fn execute(mut action: impl FnMut()) {
    action();
}
```

### Возврат из функций

Вернуть замыкание напрямую нельзя — компилятор не знает его точный размер. Поэтому мы возвращаем его через `impl Trait` с обязательным словом `move` (чтобы забрать переменные с собой):

```rust
fn create_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}
```

> [!WARNING]
> **Ограничение `impl Trait`:** Этот способ работает только тогда, когда функция возвращает **один и тот же конкретный тип замыкания во всех ветках выполнения**. Например, следующий код не скомпилируется:
> 
> ```rust
> fn returns_closure(cond: bool) -> impl Fn(i32) -> i32 {
>     if cond {
>         |x| x
>     } else {
>         |x| x * 3 // ОШИБКА: Две ветки создают два разных анонимных типа замыкания!
>     }
> }
> ```
> 
> Для обхода этого ограничения (если замыкания зависят от рантайм-условий) их упаковывают в кучу под динамический указатель `Box<dyn Trait>`:
> 
> ```rust
> fn returns_closure(cond: bool) -> Box<dyn Fn(i32) -> i32> {
>     if cond {
>         Box::new(|x| x)
>     } else {
>         Box::new(|x| x * 3)
>     }
> }
> ```

### Хранение в структурах

Если структура должна хранить замыкание, у нас есть два пути:
1. **Дженерики (быстро):** Структура жестко привязывается к типу замыкания во время компиляции. Нет накладных расходов.
   ```rust
   struct Runner<F: Fn()> { action: F }
   ```
2. **Box (гибко):** Замыкание прячется в кучу под динамический указатель. Позволяет хранить разные замыкания в одном типе структуры.
   ```rust
   struct Runner { action: Box<dyn Fn()> }
   ```

---

## 5. Что происходит под капотом компилятора?

Замыкания в Rust — это просто удобный синтаксический сахар. На самом деле компилятор превращает каждое замыкание в обычную структуру:

1. Создается **анонимная структура**, полями которой становятся все захваченные переменные.
2. Для этой структуры пишется реализация вызова (методы трейтов `call`, `call_mut` или `call_once`).
3. В месте вызова замыкания компилятор просто подставляет обычный вызов метода этой структуры.

---

## 6. Времена жизни на лету (HRTBs)

Иногда возникает сложная ситуация: замыкание принимает ссылку на объект, но время жизни этой ссылки рождается и умирает прямо внутри вызывающей функции. В таких случаях замыкание должно уметь работать со ссылками **любого** времени жизни.

Для этого используется синтаксис Higher-Rank Trait Bounds (или HRTBs) через ключевое слово `for<'a>`:

```rust
fn run_with_ref<F>(f: F)
where F: for<'a> Fn(&'a str) 
{
    let local = String::from("данные");
    f(&local); // Время жизни ссылки ограничено только этой функцией!
}
```

Конструкция `for<'a> Fn(&'a str)` говорит компилятору: «Это замыкание умеет принимать ссылку на строку с абсолютно любым временем жизни `'a`, которое мы дадим ему в момент вызова».

Давайте посмотрим, как работают замыкания разного типа, на интерактивных примерах ниже:


### 💻 Замыкания в Rust


#### Шаг 1

```rust
fn call_with_five(f: fn(i32) -> i32) {
    println!("Вызов fn pointer: {}", f(5));
}

fn main() {
    // 1. Указатель на обычную функцию
    fn add_one(x: i32) -> i32 { x + 1 }
    call_with_five(add_one);
    
    // 2. Замыкание с захватом внешнего окружения
    let coefficient = 10;
    let closure = |x| x * coefficient;
    
    // call_with_five(closure); // Ошибка! Замыкание с состоянием не приводится к fn pointer.
    println!("Вызов замыкания: {}", closure(5));
}
```

**Указатели на функции и замыкания**
- Указатель на функцию `fn` является "тонким" указателем: он хранит только адрес машинного кода.
- Замыкание (анонимная функция) может захватывать внешние переменные и нести с собой состояние.
- Замыкания получают уникальный анонимный тип, генерируемый компилятором.


#### Шаг 2

```rust
fn call_fn<F: Fn()>(f: F) { f(); }
fn call_fn_mut<F: FnMut()>(mut f: F) { f(); }
fn call_fn_once<F: FnOnce()>(f: F) { f(); }

fn main() {
    let mut greeting = "Hello".to_string();
    
    // Fn: только читает окружение
    let closure_fn = || println!("Fn: {}", greeting);
    call_fn(closure_fn);
    
    // FnMut: изменяет окружение
    let closure_mut = || {
        greeting.push_str(" World");
        println!("FnMut: {}", greeting);
    };
    call_fn_mut(closure_mut);
    
    // FnOnce: поглощает (перемещает) владение greeting
    let closure_once = move || {
        let consumed = greeting;
        println!("FnOnce: {}", consumed);
    };
    call_fn_once(closure_once);
    // call_fn_once(closure_once); // Ошибка! Переменная closure_once уже израсходована.
}
```

**Трейты Fn, FnMut и FnOnce**
- `Fn`: захват по разделяемой ссылке `&self`. Допускает вызовы в нескольких потоках параллельно.
- `FnMut`: захват по эксклюзивной ссылке `&mut self`. Позволяет модифицировать состояние.
- `FnOnce`: поглощает состояние. Может быть вызвано ровно один раз.


#### Шаг 3

```rust
fn get_counter(mut start: u32) -> impl FnMut() -> u32 {
    move || {
        start += 1;
        start
    }
}

fn main() {
    let mut next = get_counter(100);
    println!("Счетчик: {}", next());
    println!("Счетчик: {}", next());
}
```

**Возврат замыканий из функций**
- Так как типы замыканий анонимны, мы используем синтаксис `impl Trait`.
- Мы обязаны использовать ключевое слово `move`, чтобы передать владение захваченными переменными.
- Иначе возвращаемое замыкание ссылалось бы на локальные переменные функции, которые уничтожаются при выходе.


#### Шаг 4

```rust
fn process_borrowed<F>(f: F)
where
    F: for<'a> Fn(&'a str),
{
    let local_string = String::from("Локальные данные");
    f(&local_string); // Ссылка живет только внутри функции
}
fn main() {
    // Замыкание принимает ссылку на строку, время жизни которой определено внутри process_borrowed
    let print_len = |s: &str| println!("Длина строки: {}", s.len());
    process_borrowed(print_len);
}
```




---

## Проверь свои знания!

Пройдите короткий интерактивный тест, чтобы закрепить понимание замыканий в Rust.


### 📝 Замыкания в Rust


**1. В чем главное низкоуровневое различие между указателем на функцию `fn` и замыканием?**

- [ ] Указатель `fn` работает медленнее замыканий
- [ ] Указатель `fn` является тонким указателем и не может содержать внешнее состояние (контекст), тогда как замыкание может захватывать переменные из окружения
- [ ] Замыкания всегда размещаются в куче (heap), а указатели `fn` — на стеке

**2. Какова иерархия наследования (подтипов) между трейтами замыканий?**

- [ ] FnOnce является подтипом для FnMut, который является подтипом для Fn
- [ ] Fn является подтипом для FnMut, который является подтипом для FnOnce
- [ ] Эти трейты независимы и не связаны друг с другом

**3. Что произойдет, если к замыканию применить ключевое слово `move`?**

- [ ] Замыкание переместится в другой поток выполнения
- [ ] Все внешние переменные, упомянутые в замыкании, будут принудительно захвачены по значению (владение переместится внутрь замыкания)
- [ ] Компилятор запретит вызывать замыкание более одного раза

**4. Для чего в сигнатурах замыканий применяются Higher-Rank Trait Bounds (HRTBs, синтаксис `for<'a>`)?**

- [ ] Для параллельного выполнения замыкания в нескольких потоках
- [ ] Для снятия ограничений на рекурсивный вызов замыканий
- [ ] Для связывания времен жизни так, чтобы замыкание могло принимать ссылки с абсолютно любым временем жизни, определяемым в рантайме в момент вызова

