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: потеря управляемости кода.
Чрезмерное использование замыканий затрудняет понимание источников данных и их изменений, особенно когда замыкание объявлено далеко от места вызова. Держите логику ближе и не злоупотребляйте.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ