Замыкания


Замыкание (closure) представляет объект функции, который запоминает свое лексическое окружение даже в том случае, когда она выполняется вне своей области видимости.

Технически замыкание включает три компонента:

внешняя функция, которая определяет некоторую область видимости и в которой определены некоторые переменные и параметры - лексическое окружение

переменные и параметры (лексическое окружение), которые определены во внешней функции

вложенная функция, которая использует переменные и параметры внешней функции

В языке C# реализовать замыкания можно разными способами - с помощью локальных функций и лямбда-выражений.

Рассмотрим создание замыканий через локальные функции:

var fn = Outer(); // fn = Inner, так как метод Outer возвращает функцию Inner
// вызываем внутреннюю функцию Inner
fn(); // 6
fn(); // 7
fn(); // 8

Action Outer() // метод или внешняя функция
{
int x = 5; // лексическое окружение - локальная переменная
void Inner() // локальная функция
{
x++; // операции с лексическим окружением
Console.WriteLine(x);
}
return Inner; // возвращаем локальную функцию
}
Здесь метод Outer в качестве возвращаемого типа имеет тип Action, то есть метод возвратить функцию, которая не принимает параметров и имеет тип void.

Action Outer()
Внутри метода Outer определена переменная x - это и есть лексическое окружение для внутренней функции:

int x = 5;
Также внутри метода Outer определена внутренняя функция - локальная функция Inner, которая обращается к своему лексическому окружению - переменной x - увеличивает ее значение на единицу и выводит на консоль:

void Inner()
{
x++;
Console.WriteLine(x);
}
Эта локальная функция возвращается методом Outer:

return Inner;
В программе вызываем метод Outer и получаем в переменную fn локальную функцию Inner:

var fn = Outer();
Переменная fn и представляет собой замыкание, то есть объединяет две вещи: функцию и окружение, в котором функция была создана. И несмотря на то, что мы получили локальную функцию и можем ее вызывать вне ее метода, в котором она определена, тем не менее она запомнила свое лексическое окружение и может к нему обращаться и изменять, что мы увидим по консольному выводу:

fn(); // 6
fn(); // 7
fn(); // 8
Реализация с помощью лямбда-выражений
С помощью лямбд можно сократить определение замыкания:

var outerFn = () =>
{
int x = 10;
var innerFn = () => Console.WriteLine(++x);
return innerFn;
};

var fn = outerFn(); // fn = innerFn, так как outerFn возвращает innerFn
// вызываем innerFn
fn(); // 11
fn(); // 12
fn(); // 13
Применение параметров
Кроме внешних переменных к лексическому окружению также относятся параметры окружающего метода. Рассмотрим использование параметров:

var fn = Multiply(5);

Console.WriteLine(fn(5)); // 25
Console.WriteLine(fn(6)); // 30
Console.WriteLine(fn(7)); // 35

Operation Multiply(int n)
{
int Inner(int m)
{
return n * m;
}
return Inner;
}
delegate int Operation(int n);
Здесь внешняя функция - метод Multiply возвращает функцию, которая принимает число int и возвращает число int. Для этого определен делегат Operation, который будет представлять возвращаемый тип:

delegate int Operation(int n);
Хотя также можно было бы использовать встроенный делегат Func<int, int>.

Вызов метода Multiply() возвращает локальную функцию, которая соответствует сигнатуре делегата Operation:

int Inner(int m)
{
return n * m;
}
Эта функция запоминает окружение, в котором она была создана, в частности, значение параметра n. Кроме того, сама принимает параметр и возвращает произведение параметров n и m.

В итоге при вызове метода Multiply определяется переменная fn, которая получает локальную функцию Inner и ее лексическое окружение - значение параметра n:

var fn = Multiply(5);
В данном случае параметр n равен 5.

При вызове локальной функции, например, в случае:

Console.WriteLine(fn(6)); // 30
Число 6 передается для параметра m локальной функции, которая возвращает произведение n и m, то есть 5 * 6 = 30.

Также можно было бы сократить весь этот код с помощью лямбд:

var multiply = (int n) => (int m) => n * m;

var fn = multiply(5);

Console.WriteLine(fn(5)); // 25
Console.WriteLine(fn(6)); // 30
Console.WriteLine(fn(7)); // 35