Введение
Замыкание, как правило, используется функциональными языками программирования, где они связывают функцию с определенным типом параметров, это позволяет дать доступ к переменным, находящимся за пределами границы функции. С использованием делегатов замыкание доступно в С#.
Что такое Замыкание?
Чаще всего, лексика замыкания используется в функциональных языках программирования. Замыкание – это специальный тип функции, с помощью которого она ссылается на свободные переменные. Это позволяет замкнутым функциям использовать переменные из внешнего окружения, несмотря на то что они не входят в границы. Когда функция создана, внешние переменные, которыми мы пользуемся, «захватываются», иными словами, они связаны с замкнутой функцией, так что они становятся доступными. Часто это обозначает то, что делаются копии значений переменных, когда инициализируется замыкание.
Использование замыкания в С#
В С# замыкание может быть создано с помощью анонимного метода или лямбда-выражения, все зависит от версии .NET framework, на которой вы разрабатываете. Когда вы создаете функцию, переменные, что используются в ней и находятся за областью видимости, скопированы и хранятся в коде с замыканием. Они могут использоваться везде, где вы вызовете оператор delegate. Это дает огромную гибкость при использовании делегатов, но также создает возможность неожиданных багов. К этому мы вернемся позже. А пока, давайте рассмотрим простой пример замыкания.
В коде, который ниже, мы создаем переменную «nonLocal» типа integer. Во второй строчке создаем экземпляр делегата «Action», что выводит в сообщение значение переменной типа integer. В конце мы запускаем функцию-делегат, чтобы увидеть сообщения.
int nonLocal = 1;
Action closure = delegate
{
Console.WriteLine("{0} + 1 = {1}", nonLocal, nonLocal + 1);
};
closure(); // 1 + 1 = 2
Мы можем сделать то же самое с лямбда-выражением. В следующем коде мы используем «lambda» для вывода информации, при этом лямбда-выражение имеет одинаковую силу.
int nonLocal = 1;
Action closure = () =>
{
Console.WriteLine("{0} + 1 = {1}", nonLocal, nonLocal + 1);
};
closure(); // 1 + 1 = 2
Замыкания и переменные за пределами
С помощью анонимных методов или лямбда-выражения примеры выше,при этом получаем те результаты, что вы могли ожидать, так как захват переменных замыканием не очевиден сразу же. Мы можем сделать его более явным, изменяя пределы делегатов.
Рассмотрим следующий код. Здесь замыкание находится в классе «program» с переменной «action». В главном методе вызываем метод «SetUpClosure» для инициализации замыкания перед его использованием. Метод «SetUpClosure» очень важен. Вы можете увидеть, что переменная типа integer создана и инициализирована, и только тогда используется замыкание. В конце метода «SetUpClosure» эта переменная типа integer выходит за пределы. Однако, мы все еще вызываем делегат после этого. Скомпилируется и запустится ли этот код правильно? Произошло ли исключение при получении доступа к переменной за пределами? Попробуйте выполнить код.
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» была охвачена или «замкнута» кодом delegate, в результате чего она остается в нормальных пределах. По сути, переменная будет доступна, пока никаких дальнейших ссылок на делегат не останется.
Несмотря на то, что мы увидели замыкание в действии, они не поддерживаются С# и .NET framework. То, что действительно происходит - это работа на заднем фоне компилятора. Когда вы создаете собственные проекты, компилятор генерирует новые, скрытые классы, инкапсулируют нелокальную переменную и описанный код в анонимный метод или лямбда-выражение. Код, описанный в методе, и нелокальная переменная представлены в виде полей. Этот новый метод класса вызовется, когда делегат выполняется.
Автоматически сгенерированный класс для нашего простого замыкания - аналогичный приведенному ниже:
[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
public int nonLocal;
public void b__0()
{
Console.WriteLine("{0} + 1 = {1}", this.nonLocal, this.nonLocal + 1);
}
}
Відео курси за схожою тематикою:
Замыкание захватывает переменную, а не его значение
В некоторых языках программирования определяют значение переменной, которая используется в замыкании. В С# захватываются сами переменные. Это важное отличие, так как мы можем изменять значение переменной за пределами функции. Для иллюстрации рассмотрим следующий код. Здесь мы создаем замыкание, которое выводит наше начальное математическое значение переменной. При создании делегатов значение переменной типа integer равно 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, который имеет 5 новых потоков. Каждая пауза короткая, перед выводом значения переменной внутри цикла. Если значение переменной в цикле были захвачены, мы увидим цифры от 1 до 5 показаны в консоли, хотя, возможно, не в правильном порядке. Однако, так как эта переменная находится внутри замыкания и цикл закончится до того, как переменные будут выведены в сообщение, в конечном итоге мы увидим значение 6 для каждого потока.
for(int i = 1; i <= 5; i++)
{
new Thread(delegate()
{
Thread.Sleep(100);
Console.Write(i);
}).Start();
}
// Outputs "66666"
К счастью, такая проблема легко устраняется, когда вы понимаете, что переменные, а не их значения захватываются. Все, что нам нужно сделать, это создать новую переменную для каждого прохождения(итерации) цикла. Это объявление можно записать в теле цикла и давать значение в управляющую переменную. При нормальных обстоятельствах временная переменная будет находится за переделами, когда цикл закончится, но замыкание будет связывать и поддерживать ее.
Безкоштовні вебінари за схожою тематикою:
В коде ниже вы можете увидеть 5 примеров «значений», переменные, созданные и им назначенные 5 различных значений, каждая из них привязана к разному потоку.
for(int i = 1; i <= 5; i++)
{
int value = i;
new Thread(delegate()
{
Thread.Sleep(100);
Console.Write(value);
}).Start();
}
// Outputs "12345"
Обратите внимание: вывод может меняться в зависимости от порядка, в котором потоки выполняются.
Статті за схожою тематикою