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, тощо).
Важливо: змінні захоплюються не за значенням, а за посиланням! Якщо ми змінюємо змінну в замиканні, вона зміниться й «ззовні». Насправді компілятор 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 = "Привіт";
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: втрата керованості коду.
Надмірне використання замикань ускладнює розуміння джерел даних і їх змін, особливо коли замикання оголошене далеко від місця виклику. Тримайте логіку ближче до місця використання й не зловживайте.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ