Замыкание (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