JavaRush /Курсы /C# SELF /Замыкания

Замыкания

C# SELF
51 уровень , 3 лекция
Открыта

1. Введение

В программировании замыкание (closure) — это не способ закрыть дверь в JavaScript, а механизм, при котором лямбда-выражение или анонимный метод захватывает переменные из окружающего контекста и «помнит» их даже после завершения блока, где они были объявлены. Проще говоря, замыкание — это функция, которая запомнила условия, в которых родилась, и хранит эти значения, как маленький чемоданчик с личными вещами (переменными).

Замыкание — это функция вместе с окружением (scope), которое существовало в момент её создания.

Простейший пример замыкания

Давайте разберём на практике:


Func<int> MakeCounter()
{
    int count = 0;
    return () =>
    {
        count++;
        return count;
    };
}

Вызовем так:


var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3

Как это работает?

  • Переменная count объявлена внутри метода MakeCounter.
  • Лямбда () => { ... } возвращается наружу и живёт теперь вне метода.
  • Но! Она помнит переменную count, хотя сам метод MakeCounter уже давно завершился.

Вот это и есть замыкание: лямбда «замкнула» (захватила) переменную count из окружающего контекста.

Что именно «захватывает» лямбда?

  • локальные переменные из окружающего метода (scope),
  • параметры метода,
  • переменные в блоках (for, foreach, etc.).

Важно: переменные захватываются не по значению, а по ссылке! Если мы меняем переменную в замыкании, она изменится и «снаружи». На самом деле компилятор C# создаёт специальный вспомогательный класс для этих переменных — но об этом достаточно помнить концептуально, чтобы уверенно пользоваться замыканиями.

2. Замыкание и лексическая область видимости

Доработаем пример «фабрики функций» с замыканием:


Func<int, int> PowerFactory(int power)
{
    return x =>
    {
        int result = 1;
        for (int i = 0; i < power; i++)
            result *= x;
        return result;
    };
}

Использование:


var square = PowerFactory(2);   // x^2
var cube = PowerFactory(3);     // x^3

Console.WriteLine(square(5)); // 25
Console.WriteLine(cube(2));   // 8

Функции square и cube созданы с разными значениями переменной power, и каждая помнит своё. Для каждого вызова PowerFactory создаётся «собственный рюкзак» захваченных значений.

3. Мутация захваченных переменных

Иногда возникает вопрос: что будет, если в цикле создавать несколько лямбд, которые захватывают переменную из цикла? Здесь легко наступить на грабли.

Пример: замыкание в цикле


var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
    action(); // ???

Что вы ожидаете? 0, 1, 2? На самом деле будет так:

3
3
3

Почему так? Все лямбды «ссылаются» на одну и ту же переменную i. К концу цикла i уже равна 3, и именно это значение увидят все наши Action.

Исправленный вариант

Чтобы каждая лямбда захватывала «своё» значение, создаём новую переменную внутри тела цикла:


var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
    action(); // 0 1 2

Теперь copy — это новая переменная на каждой итерации, и замыкание захватывает именно её.

4. Применение замыканий в реальных задачах

Обработка данных и коллбэки

Когда вы делаете что-то асинхронное или откладываете выполнение (обработчики событий, фильтрация, планирование задач) — замыкание позволяет «упаковать» логику вместе с параметрами. Например:


void ProcessList(List<int> list, int threshold)
{
    var filtered = list.Where(x => x > threshold);
    foreach (var item in filtered)
        Console.WriteLine(item);
}

Здесь лямбда внутри Where захватывает переменную threshold.

Создание «фабрик» функций

Передаём параметр — получаем функцию со «встроенным» этим параметром. Такой приём удобен для настройки фильтрации, компараторов сортировки, реакций UI и т.д.

Управление состоянием

Иногда нужно хранить немного состояния без отдельного класса:


Func<string, string> CreateGreeting()
{
    string prefix = "Hello";
    return name =>
    {
        return $"{prefix}, {name}!";
    };
}

5. Полезные нюансы

Под капотом: как работает замыкание в C#

Всё, что захвачено лямбдой, компилятор превращает во вспомогательный класс: переменные становятся полями, а лямбда — методом. Поэтому состояние переменных «живёт» между вызовами.

Каждая «фабрика» порождает маленький объект. Это нормально — .NET эффективно управляет такими объектами и выделяет их только когда это действительно нужно.

Памятка: не захватывайте «большие» объекты без необходимости

Если замкнуть крупный объект (например, форму UI), он не будет освобождён, пока жива лямбда. Классическая причина утечек — подписка на события через лямбду с захваченным «тяжёлым» контекстом и отсутствие отписки (+=/-=).

Замыкания и жизнь коллекций — пример с LINQ

Замыкания делают LINQ гибким: фильтры помнят свои параметры.


List<string> colors = new List<string> { "Red", "Green", "Blue", "Yellow" };
string startsWith = "B";
var filtered = colors.Where(c => c.StartsWith(startsWith));
foreach (var color in filtered)
    Console.WriteLine(color); // "Blue"

Если затем изменить startsWith, изменится и результат:


startsWith = "R";
foreach (var color in filtered)
    Console.WriteLine(color); // "Red"

Так происходит потому, что замыкание ссылается на ту же переменную startsWith, а метод StartsWith каждый раз проверяет текущее значение.

6. Типичные ошибки при работе с замыканиями

Ошибка №1: захват переменной, которая меняется до момента использования.
Классическая ситуация в циклах: лямбда внутри замыкания «смотрит» на одну и ту же переменную цикла, которая к моменту вызова уже получила другое значение. В итоге функция работает не с теми данными, что ожидались. Лечится введением отдельной переменной внутри цикла.

Ошибка №2: захват слишком большого контекста.
Замыкание тащит за собой весь объект, вместо конкретного поля/значения. Это создаёт лишние зависимости и усложняет код. Захватывайте только нужное.

Ошибка №3: удержание тяжёлых ресурсов и утечка памяти.
Подписка на событие лямбдой, которая замкнула «тяжёлый» объект, и отсутствие отписки — объект не освобождается. Следите за временем жизни подписок и используйте явную отписку (-=).

Ошибка №4: потеря управляемости кода.
Чрезмерное использование замыканий затрудняет понимание источников данных и их изменений, особенно когда замыкание объявлено далеко от места вызова. Держите логику ближе и не злоупотребляйте.

2
Задача
C# SELF, 51 уровень, 3 лекция
Недоступна
Создание простой функции со встроенными данными (замыкание)
Создание простой функции со встроенными данными (замыкание)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ