← Назад к вопросам

Что такое замыкание в коде?

1.0 Junior🔥 151 комментариев
#Основы C# и .NET

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое замыкание (Closure)?

Замыкание — это функция, которая «запоминает» окружение, в котором она была создана, даже после того, как это окружение перестало существовать. Это позволяет функции обращаться к переменным из внешней (охватывающей) области видимости, даже когда выполнение кода уже вышло из этой области. В контексте C# замыкания тесно связаны с лямбда-выражениями и анонимными методами.

Механизм работы замыканий в C#

Когда компилятор C# обнаруживает лямбда-выражение или анонимный метод, который захватывает переменные из внешней области видимости, он автоматически генерирует скрытый класс-замыкание. Этот класс содержит:

  1. Поля для захваченных переменных.
  2. Метод, соответствующий телу лямбды/анонимного метода.

Рассмотрим пример:

using System;

class Program
{
    static void Main()
    {
        // Внешняя переменная, которая будет захвачена
        int multiplier = 3;
        
        // Лямбда-выражение, захватывающее переменную multiplier
        Func<int, int> multiplierFunc = x => x * multiplier;
        
        // Изменяем захваченную переменную
        multiplier = 5;
        
        // Вызываем замыкание
        Console.WriteLine(multiplierFunc(4)); // Выведет 20, а не 12!
    }
}

Ключевые особенности:

  • Захват по ссылке: В C# замыкания захватывают переменные, а не их значения. Это значит, что если изменить захваченную переменную после создания замыкания, лямбда "увидит" новое значение.
  • Продление времени жизни: Обычно локальные переменные уничтожаются после выхода из метода. Но если переменная захвачена замыканием, её время жизни продлевается до тех пор, пока существует замыкание.
  • Неявное создание класса: Компилятор создаёт класс, подобный этому:
// Пример того, что генерирует компилятор
private sealed class ClosureClass
{
    public int multiplier;
    
    public int Method(int x)
    {
        return x * multiplier;
    }
}

Практическое применение замыканий

1. Отложенное выполнение

// Создаём действие, которое запоминает текущее время
var time = DateTime.Now;
Action logTime = () => Console.WriteLine($"Создано в: {time}");

// Вызываем позже - время сохранится
Task.Delay(1000).Wait();
logTime(); // Выведет время создания, а не текущее

2. Создание специализированных функций

// Фабрика функций для фильтрации по минимальному значению
Func<int, Predicate<int>> createMinFilter = minValue => 
    value => value >= minValue;

var filterAbove10 = createMinFilter(10);
var filterAbove20 = createMinFilter(20);

Console.WriteLine(filterAbove10(15)); // True
Console.WriteLine(filterAbove20(15)); // False

3. Обработка событий с дополнительным контекстом

button.Click += (sender, e) => 
{
    // Захватываем локальную переменную
    var clickTime = DateTime.Now;
    MessageBox.Show($"Клик в {clickTime}");
};

Важные предостережения

Проблема с циклами

Самая распространённая ошибка — захват переменной цикла:

var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (var action in actions)
{
    action(); // Выведет три раза "3", а не "0", "1", "2"!
}

Решение: создать локальную копию внутри цикла:

for (int i = 0; i < 3; i++)
{
    int temp = i; // Локальная копия для каждой итерации
    actions.Add(() => Console.WriteLine(temp));
}

Утечки памяти

Поскольку замыкание продлевает время жизни захваченных переменных, это может привести к утечкам памяти, особенно если замыкание живёт долго (например, является обработчиком события).

Заключение

Замыкания в C# — мощный инструмент функционального программирования, который позволяет:

  • Инкапсулировать состояние вместе с поведением
  • Создавать специализированные функции на лету
  • Реализовывать паттерны вроде каррирования и частичного применения функций

Понимание замыканий критически важно для эффективного использования LINQ, асинхронного программирования и современных шаблонов C#. Однако важно помнить о захвате по ссылке и потенциальных проблемах с производительностью, когда замыкания захватывают большое количество переменных или переменные большого размера (например, коллекции).

Что такое замыкание в коде? | PrepBro