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, тощо).

Важливо: змінні захоплюються не за значенням, а за посиланням! Якщо ми змінюємо змінну в замиканні, вона зміниться й «ззовні». Насправді компілятор 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: втрата керованості коду.
Надмірне використання замикань ускладнює розуміння джерел даних і їх змін, особливо коли замикання оголошене далеко від місця виклику. Тримайте логіку ближче до місця використання й не зловживайте.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ