Функциональное программирование 3 - лямбда функции и замыкания

В предыдущей статье были рассмотрены различные варианты синтаксиса для передачи одной функции в другую. Понятно, что можно заранее объявить функцию, и затем передать ее. Но довольно часто было бы удобнее описать передаваемую функцию прямо в списке передаваемых аргументов.
Функции, объявляемые "по месту их использования", называются лямбда-функциями (или лямбда-выражениями). В отличие от обычных функций, лямбды могут не иметь имени.

В C# разных версий лямбды эволюционировали до современного вида, пожалуй самого удобного из всех известных мне вариантов.

int[] ary = { 1, 2, 3 };
var x = 2;
var ary1 = ary.Select(elem => elem * x;);


Выражение "elem => elem*x" - типичная лямбда-функция. В нем до оператора => перечисляется список аргументов, после - собственно выражение, результат вычисления которого является возвращаемым значениям лямбды. Все лямбды передаются как делегаты.

В других языках синтаксис лямбд более громоздкий. Обычно используется ключевое слово типа function, lambda и т.д., после которого указывается список аргументов, а затем - тело функции. В C# роль определяющего ключевого слова играет оператор =>.

В данном примере лямбда-функции есть еще одна интересная особенность:  лямбда ссылается на локальную переменную x, объявленную вне самой лямбды. Такие функции называются замыканиями.

В общем случае замыкание — это функция, определенная в теле другой функции (т.е. не только лямбда, но и просто обычная вложенная функция). При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции.

Интересно, что вложенные функции существовали еще во времена Turbo Pascal. Уже тогда вложенные функции имели доступ к локальным переменным объемлющих функций (т.е. они как-бы являлись замыканиями), и были доступны только внутри объемлющих функций. Их реализация была довольно простой, потому что в Паскале не было делегатов. Дело в том, что с делегатами и замыканиями связана одна особенность: за счет делегатов вложенная функция может иметь время жизни дольше, чем объемлющая. Такое может быть, например, если объемлющая функция возвращает вложенную как результат своей работы.

В классическом Си можно попытаться возвратить, например, адрес локальной переменной. Адрес будет возвращен, но любые действия с ним после выхода из функции приведут к ошибке, так как стекового фрейма с этой локальной переменной уже не существует. Примерно такая же ситуация может возникнуть и с возвратом делегата, ссылающегося на вложенную функцию, которая использует локальную переменную объемлющей функции. Для решения этой проблемы можно воспользоваться разными подходами, например - размещение замкнутых переменных в динамической памяти со сборщиком мусора. В любом случае, замкнутые переменные уже не должны быть стековыми.

No comments:

Post a Comment