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