JavaRush /Курсы /C# SELF /Захват переменных ( Closures...

Захват переменных ( 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($"Task #{index}"));
}

Без локальной переменной все задачи напечатают один и тот же номер, что обычно не то, чего мы хотели.

Пример 3: LINQ-запросы

LINQ к коллекциям часто использует замыкания, чтобы фильтровать или преобразовывать элементы с учетом переменных из внешней области. Например:

string prefix = "Task";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));

Здесь лямбда в Where запомнила значение prefix и вызывает метод StartsWith.

7. Особенности, ограничения и типичные ошибки при работе с замыканиями

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

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

Ошибка №3: долгоживущие делегаты с замыканиями.
Если делегат с замыканием сохраняется на долгое время (например, в статическом поле), переменные, на которые он ссылается, тоже остаются в памяти дольше, чем ожидается. Это часто становится причиной скрытых утечек памяти и проблем с производительностью.

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