Замыкания C#


Замыкания обычно ассоциируются с функциональными языками программирования, где они связывают функцию с ее ссылочным окружением, позволяя получить доступ к переменным за пределами области видимости функции. С помощью делегатов замыкания доступны и в C#.

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

Лексические замыкания чаще всего используются в функциональных языках программирования. Замыкание - это особый тип функции, которая связана со своим ссылочным окружением, то есть с кодом, который ее определил. Это позволяет закрывающим функциям использовать переменные из этого ссылочного окружения, несмотря на то, что эти значения не входят в область видимости закрывающей функции. Когда функция создается, внешние переменные, которые она использует, "закрываются", то есть привязываются к закрывающей функции таким образом, чтобы сделать их доступными. Часто это означает, что при определении замыкания создаются копии их значений.

Использование замыканий в C#

В C# замыкания можно создавать с помощью анонимных методов и лямбда-выражений, в зависимости от версии фреймворка .NET, в котором вы ведете разработку. Когда вы создаете функцию, переменные, которые она будет использовать и которые находятся вне ее видимой области видимости, по сути, копируются и сохраняются вместе с кодом замыкания. Затем они могут быть использованы при каждом вызове делегата. Это обеспечивает большую гибкость при использовании таких делегатов, но также создает возможность возникновения неожиданных ошибок. Мы рассмотрим их позже в этой статье. А пока давайте рассмотрим простой замыкание.

В приведенном ниже коде мы создаем целочисленное значение в переменной "nonLocal". Второй оператор создает экземпляр делегата Action, который выводит сообщение, использующее значение целочисленной переменной. Наконец, мы запускаем делегат, чтобы увидеть появление сообщения. Результаты, вероятно, именно такие, каких вы ожидаете, независимо от того, привыкли вы использовать замыкания или нет.

int nonLocal = 1;
Action closure = delegate
{
    Console.WriteLine("{0} + 1 = {1}", nonLocal, nonLocal + 1);
};
closure(); // 1 + 1 = 2
То же самое мы можем сделать с помощью лямбда-выражения. Здесь мы используем лямбду оператора для вывода информации, но лямбда выражения также подходит.

int nonLocal = 1;
Action closure = () =>
{
    Console.WriteLine("{0} + 1 = {1}", nonLocal, nonLocal + 1);
};
closure(); // 1 + 1 = 2
Замыкания и внепространственные переменные

В примерах с анонимным методом или лямбда-выражением, приведенных выше, результаты будут такими, как вы и ожидали, потому что захват переменных замыканием не сразу бросается в глаза. Мы можем сделать это более очевидным, изменив область видимости делегата.

Рассмотрим следующий код. Здесь замыкание хранится в переменной Action на уровне класса. Метод Main вызывает метод SetUpClosure, чтобы инициализировать замыкание перед его выполнением. Метод SetUpClosure очень важен. Вы можете видеть, что создается и инициализируется целочисленная переменная, которая затем используется в закрытии. По завершении метода SetUpClosure эта целочисленная переменная выходит из области видимости. Однако мы все еще вызываем делегат после этого. Теперь результат не так очевиден. Будет ли он правильно скомпилирован и запущен? Возникнет ли исключение при попытке доступа к переменной, выходящей за пределы области видимости? Попробуйте выполнить этот код.

class Program
{
    static Action _closure;
 
    static void Main(string[] args)
    {
        SetUpClosure();
        _closure(); // 1 + 1 = 2
    }
 
    private static void SetUpClosure()
    {
        int nonLocal = 1;
        _closure = () =>
        {
            Console.WriteLine("{0} + 1 = {1}", nonLocal, nonLocal + 1);
        };
    }
}
Вы можете видеть, что мы получаем тот же результат, что и в исходном примере. Это и есть замыкание в действии. Переменная "nonLocal" была захвачена, или "закрыта", кодом делегата, в результате чего она остается в области видимости за пределами обычных границ. Фактически, она будет оставаться доступной до тех пор, пока не останется ни одной ссылки на делегат.

Хотя мы видели, как работают замыкания, на самом деле они не поддерживаются C# и фреймворком .NET. На самом деле это происходит благодаря закулисной работе компилятора. Когда вы собираете проект, компилятор генерирует новый скрытый класс, который инкапсулирует нелокальные переменные и код, который вы включаете в анонимный метод или лямбда-выражение. Код включается в метод, а нелокальные переменные представляются в виде полей. Метод этого нового класса вызывается при выполнении делегата.

Автоматически сгенерированный класс для нашего простого замыкания выглядит следующим образом:

[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
    public int nonLocal;
 
    public void <SetUpClosure>b__0()
    {
        Console.WriteLine("{0} + 1 = {1}", this.nonLocal, this.nonLocal + 1);
    }
}
Замыкания захватывают переменные, а не значения

Некоторые языки программирования захватывают значения переменных, используемых в замыканиях, когда эти замыкания определены. C# захватывает сами переменные. Это важное различие, поскольку оно может привести к изменению значений за границей области видимости. Для примера рассмотрим следующий код. Здесь мы создаем замыкание, которое выводит наше знакомое математическое уравнение. Когда делегат объявлен, значение целочисленной переменной равно 1. Однако после объявления замыкания, но до его выполнения, значение переменной меняется на 10.

int nonLocal = 1;
Action closure = delegate
{
    Console.WriteLine("{0} + 1 = {1}", nonLocal, nonLocal + 1);
};
 
nonLocal = 10;
 
closure();


Можно было бы ожидать, что, поскольку в момент создания замыкания нелокальная переменная имеет значение 1, результирующий вывод будет "1 + 1 = 2". Действительно, в некоторых языках программирования именно так и происходит. Однако, поскольку переменная фиксируется, изменение ее значения влияет на выполнение замыкания. Фактический результат таков:

10 + 1 = 11
Изменения нелокальной переменной замыкания передаются и в другом направлении. В следующем коде делегат изменяет значение до того, как это покажет объявляющий код. Изменение видно во внешнем коде, несмотря на то, что оно находится в другой области видимости, чем закрывающая переменная.

int nonLocal = 1;
Action closure = delegate
{
    nonLocal++;
};
closure();
 
Console.WriteLine(nonLocal); // 2
Захват переменных приводит к тому, что мы можем внести неожиданные ошибки в наш код. Мы можем продемонстрировать эту проблему на другом примере. На этот раз мы используем замыкания в распространенном сценарии: многопоточном или параллельном программировании. В приведенном ниже коде показан цикл for, который создает и запускает пять новых потоков. Каждый из них делает небольшую паузу, прежде чем вывести значение из управляющей переменной цикла. Если бы значение управляющей переменной было захвачено, мы бы увидели, как на консоль записываются числа от одного до пяти, хотя, возможно, и не в правильном порядке. Однако, поскольку эта переменная привязана к закрытию, а цикл завершается до того, как потоки выведут свои сообщения, мы фактически видим итоговое значение 6 для каждого потока.

for (int i = 1; i <= 5; i++)
{
    new Thread(delegate()
    {
        Thread.Sleep(100);
        Console.Write(i);
    }).Start();
}
 
// Выводит "66666"
К счастью, подобная проблема легко решается, если понять, что перехватываются именно переменные, а не значения. Все, что нам нужно сделать, - это ввести новый экземпляр переменной для каждой итерации цикла. Ее можно объявить в теле цикла и присвоить ей значение из управляющей переменной. При обычных обстоятельствах временная переменная выйдет из области видимости при завершении цикла, но замыкание привяжется к ней и сохранит ее.

В приведенном ниже коде создаются пять экземпляров переменной "value", которым присваиваются пять различных значений, каждое из которых привязывается к отдельному потоку.

for (int i = 1; i <= 5; i++)
{
    int value = i;
    new Thread(delegate()
    {
        Thread.Sleep(100);
        Console.Write(value);
    }).Start();
}
 
// Выводит "12345".
Примечание: Вывод может отличаться в зависимости от порядка выполнения потоков.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегизаметки, си шарп




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.




Как написать макрос для CorelDRAW на VBA: введение
Интеграция безопасности в DevOps: советы по включению
PDO и MySQLi: битва API баз данных PHP