JavaRush /Курси /C# SELF /Захоплення змінних ( Closure...

Захоплення змінних ( Closures)

C# SELF
Рівень 49 , Лекція 4
Відкрита

1. Що таке замикання?

У програмуванні замикання (closure) — це функція, що захоплює змінні із зовнішнього контексту. Простіше: якщо лямбда-вираз або анонімний метод використовує змінні, оголошені поза його тілом, ця функція перетворюється на замикання. Воно «памʼятає», які значення мали ці змінні на мить створення.

Аналогія з життя:
Уявімо, що ви записали секретний рецепт на аркуші паперу й сховали його в конверт. Навіть якщо сам аркуш згодом загубиться або його буде важко знайти (змінна стане недоступною безпосередньо), у когось із цим конвертом (лямбдою) все ще буде доступ до рецепта.

Простий приклад:

int x = 42;
Func<int> getX = () => x;
Console.WriteLine(getX()); // 42

Тут getX — це замикання, оскільки воно використовує змінну x, оголошену поза своїм тілом.

2. Чому захоплення змінних — це важливо?

У C# замикання використовуються буквально на кожному кроці:

  • у колекціях і запитах LINQ
  • для передавання параметрів до подій чи асинхронних методів
  • під час створення обробників подій усередині циклів
  • для збереження «контексту» між різними викликами

Без замикань багато стандартних практик C# були б неможливі або виглядали б дуже незручно.

Приклад із життя

Уявімо, що ви розробляєте застосунок-нагадувач: користувач задає серію нагадувань, і згодом (через хвилину, годину, тиждень…) він має показати потрібне повідомлення. Зручно передати в обробник лямбду, яка «запамʼятала», що саме нагадати. Це і є захоплення змінних — класика.

3. Як C# реалізує захоплення змінних

Внутрішньо C# застосовує хитрий прийом: коли у вас є лямбда-вираз, що використовує зовнішні змінні, компілятор автоматично створює допоміжний клас — display class. Усі «захоплені» змінні стають полями цього класу.

Схематично це виглядає так:


Зовнішня змінна ──► DisplayClass
                         ▲
                         │
                    Замикання (лямбда)

Ілюстрація в коді

Ось що відбувається «під капотом»:

int x = 5;
Func<int> f = () => x;
// Тут компілятор робить приблизно так:
class DisplayClass
{
    public int x;
    public int Lambda() => x;
}
DisplayClass display = new DisplayClass();
display.x = 5;
Func<int> f = display.Lambda;

Це пояснює, чому замикання продовжує бачити актуальне значення змінної навіть після виходу за межі блоку, де її оголошено.

4. Значення змінної «заморожене» чи змінюється?

У C# змінні захоплюються за посиланням, а не за значенням. Це означає: якщо лямбда-вираз використовує змінну, і ця змінна змінюється в іншому місці — лямбда побачить нове значення.

Приклад:

int x = 10;
Func<int> getX = () => x;

x = 20;
Console.WriteLine(getX()); // 20, а не 10!

Студенти часто очікують, що getX() завжди поверне 10, бо змінну «захоплено». Але насправді лямбда читає змінну, яка все ще існує й яку можна змінювати.

Коли значення все ж фіксується?

Якщо змінну оголошено в циклі з новою областю видимості, наприклад за допомогою foreach, і на кожній ітерації створюється нова змінна — лямбда «запамʼятає» поточне значення.

5. Приклади: замикання в циклі — типова пастка

Поширена помилка

Хочемо створити масив делегатів, кожен із яких виводить свій номер із циклу:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    actions[i] = () => Console.WriteLine(i);
}
foreach (var action in actions)
    action();

Що виведе програма?

5
5
5
5
5

Ого! Чому не 0, 1, 2, 3, 4?

Причина:
Лямбда захоплює одну й ту саму змінну i, яка продовжує змінюватися в міру проходу циклу. Коли ви згодом викличете делегати, i уже дорівнюватиме 5.

Як правильно?

Потрібно створити окрему змінну в тілі циклу:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    int index = i; // Нова змінна для кожної ітерації!
    actions[i] = () => Console.WriteLine(index);
}
foreach (var action in actions)
    action();

Тепер програма виведе:

0
1
2
3
4

Як це повʼязано з display class?

У першому варіанті всі делегати привʼязуються до одного поля — тому результат однаковий. У другому разі створюється нова локальна змінна для кожної ітерації, а отже для кожного делегата формується окремий DisplayClass з унікальним значенням.

6. Практичні сценарії використання захоплення змінних

Приклад 1: Обробка подій із «контекстом»

Припустімо, у невеликому застосунку є список завдань, і до кожного привʼязано обробник для кнопки «Виконати». Потрібно, щоб лямбда всередині обробника «памʼятала», яке завдання обробити:

foreach (var task in tasks)
{
    button.Click += (sender, e) => CompleteTask(task);
}

Тут змінна task захоплюється на кожній ітерації. Важливо переконатися, що її правильно оголошено всередині циклу, аби не потрапити в пастку, як у прикладі вище.

Приклад 2: Асинхронні операції

Часто замикання використовують для передавання параметрів в асинхронну логіку — наприклад, збереження змінної в локальному «слоті» під час запуску асинхронного завдання:

for (int i = 0; i < 3; i++)
{
    int index = i; // Обовʼязково!
    Task.Run(() => Console.WriteLine($"Завдання №{index}"));
}

Без локальної змінної всі завдання виведуть один і той самий номер, що зазвичай не те, чого ви очікуєте.

Приклад 3: LINQ-запити

LINQ до колекцій часто використовує замикання, щоб фільтрувати або перетворювати елементи з урахуванням змінних із зовнішньої області. Наприклад:

string prefix = "Завдання";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));

Тут лямбда в Where запамʼятала значення prefix і викликає метод StartsWith.

7. Особливості, обмеження й типові помилки під час роботи із замиканнями

Помилка № 1: використання однієї змінної в циклі для всіх делегатів.
Якщо в циклі всі делегати посилаються на одну й ту саму змінну, результат буде неочікуваним. Важливо створювати нову локальну змінну всередині циклу для кожного делегата, щоб уникнути спільного посилання.

Помилка № 2: замикання на змінних поза методом.
Якщо замикання захоплює поле класу або змінну, оголошену поза поточним методом, воно утримуватиме посилання на цю змінну. Це може призвести до витоків памʼяті, адже збирач сміття не зможе звільнити обʼєкт, поки на нього є посилання із замикань.

Помилка № 3: довгоживучі делегати із замиканнями.
Якщо делегат із замиканням зберігається надовго (наприклад, у статичному полі), змінні, на які він посилається, теж залишаються в памʼяті довше, ніж очікується. Це часто стає причиною прихованих витоків памʼяті та проблем із продуктивністю.

1
Опитування
Лямбда-вирази, рівень 49, лекція 4
Недоступний
Лямбда-вирази
Синтаксис лямбда-виразів
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ