Изучение лямбда-выражений в C#

ОГЛАВЛЕНИЕ

Данная статья рассматривает синтаксис, ограничения и особенности реализации лямбда-выражений в C#

Введение

Лямбда-выражение является встраиваемым делегатом, введенным в язык C # 3.0. Это краткое представление безымянного метода. Он предоставляет синтаксис для создания и вызова функций. Хотя лямбда-выражения проще использовать, чем безымянные методы, их реализация немного отличается. И безымянные методы, и лямбда-выражения позволяют определять встраиваемую реализацию метода, однако безымянный метод явно требует определять типы параметров и тип возвращаемой переменной для метода. Лямбда-выражение использует возможность выведения типа C# 3.0, позволяющую компилятору логически выводить тип переменной на основе контекста.

Лямбда-выражение можно разделить на параметры с последующим исполнимым кодом, например:

Параметр => исполнимый код.

Левая часть представляет собой ноль или более параметров с последующим символом лямбда => который применяется для отделения объявления параметра от реализации метода. Далее за лямбда-выражением идет тело оператора.

Лямбда-выражение позволяет передавать функции в качестве аргументов вызову метода. Начнем с простого примера лямбда-выражения, возвращающего четные числа из списка целых чисел.

//простой пример лямбда-выражения.
    public static void SimpleLambdExpression()
    {
        List<int> numbers = new List<int>{1,2,3,4,5,6,7};
        var evens = numbers.FindAll(n => n % 2 == 0);
        var evens2 = numbers.FindAll((int n) => { return n % 2 == 0; });
        ObjectDumper.Write(evens);
        ObjectDumper.Write(evens2);
    }

Взглянув на первое лямбда-выражение, присвоенное переменной evens, вы заметите несколько отличий от безымянных методов. Первое – в коде нигде не используется ключевое слово delegate. Второе –  не определены типы параметров и возвращаемой переменной, потому что компилятор логически выводит тип на основе контекста. Типы в выражении определяются определением delegate. В данном случае тип возвращаемой переменной, заданный методом FindAll, принимает делегат, принимающий параметр int и возвращающий логическое значение. Лямбда-выражение без фигурных скобок и типа возвращаемой переменной является самым кратким способом представления безымянного метода. Если число параметров равно единице, то можно опустить круглые скобки, окружающие параметр, как показано в первом лямбда-выражении. Хотя лямбда-выражение не требует явных параметров, можно определить параметры, фигурные скобки и тип возвращаемой переменной, как показано во втором лямбда-выражении, присвоенном переменной even2.

Используется явный параметр int и тип возвращаемой переменной, обычно задаваемые в методе. Оператор возврата не будет работать, если не закрыть исполняемый код круглыми скобками, учитывая, что вы полностью оговорили все, что относится к методу.

Другое место, где требуются круглые скобки в лямбда-выражении, - когда вы хотите использовать параметр в нескольких блоках кода внутри лямбда-выражения следующим образом:

delegate void WriteMultipleStatements(int i);
    public static void MultipleStatementsInLamdas()
    {
        WriteMultipleStatements write = i =>
            {
                Console.WriteLine("Number " + i.ToString());
                Console.WriteLine("Number " + i.ToString());
            };
            write(1);
    }

В примере кода выше код был заключен в фигурные скобки, чтобы можно было использовать параметр в обоих выражениях. Без фигурных скобок компилятор не смог бы распознать переменную i.

Можно использовать лямбда-выражения, если делегат не имеет параметров. В таком случае надо задать пару пустых круглых скобок, чтобы обозначить метод без параметров. Ниже дан простой пример, показывающий лямбда без параметров.

delegate void LambdasNoParams();
    public static void LambdasWithNoParameter()
    {
       LambdasNoParams noparams = () => Console.WriteLine("hello");
       noparams();
    }

C# 3.0 определяет количество обобщенных делегатов, которые можно назначить лямбда-выражению вместо ключевого слова var, логически выводящего тип. Рассмотрим пример использования нескольких обобщенных делегатов:

public static void GenericDelegates()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
        Func<int, bool> where = n => n < 6;
        Func<int, int> select = n => n;
        Func<int, string> orderby = n =>  n % 2 == 0 ? "even" : "odd";
        var nums = numbers.Where(where).OrderBy(orderby).Select(select);
        ObjectDumper.Write(nums);
    }

В примере выше используются три разных расширяющих метода: where, orderby и select. Расширяющий метод where принимает обобщенный делегат с параметром int и логическим типом возвращаемой переменной, чтобы определить, будет ли конкретный элемент входить в выходную последовательность. Расширяющий метод select принимает целый параметр и возвращает целое число, но он может вернуть все, во что вы хотите превратить результат — перед отправкой в выходную последовательность. В расширяющем методе orderby принимается целый параметр и используется для определения его четности или нечетности. На основе этого сортируются результаты. Это было бы тяжело, если бы пришлось определять три разных делегата для каждого лямбда-выражения. Благодаря введению обобщенных делегатов в C# 3.0 весьма нетривиально назначать лямбда-выражения обобщенным делегатам и передавать эти делегаты расширяющим методам. Обобщенные делегаты очень удобны и помогают избежать написания общих делегатов, которые были распространены в .NET 1.1 и .NET 2.0 (так как не было готовых обобщенных делегатов). Обобщенные делегаты позволяют определять до 4 параметров и 1 тип возвращаемой переменной, поэтому используются такие делегаты:

Func<int, bool, string, double, decimal> test;

Если ваш метод или делегат не отвечает условиям, то вам придется вручную объявить делегат, принимающий эти параметры. Обобщенные делегаты обычно охватывают большинство сценариев, но в случаях, когда он не удовлетворяет ваши нужды, пишите пользовательский делегат.

Бывают случаи, когда выведение типа не возвращает тип данных, который вам нужно, чтобы возвращало лямбда-выражение. В таких случаях можно явно задать тип параметра в лямбда-выражении. Например:

Func<double, int> expr = (x) => x / 2;

Выражение выше возвращает ошибку компилятора, так как при делении double выведенный тип на самом деле будет double. Однако вы назначаете лямбда-выражение делегату, имеющему целый тип возвращаемой переменной. Если на самом деле вы хотите возвращать int из метода, преобразуйте тело выражения в int, чтобы указать ваше намерение, как показано ниже:

Func<double, int> expr = (x) => (int)x / 2;

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

//пример, показывающий два типа лямбда-выражений
public static void ExplicitParametersInLambdaExpression()
{
    Func<int, int> square = x => x * x;
    Func<int, int> square1 = (x) => { return x * x; };

    Expression<Func<int, int>> squareexpr = x => x * x;

    Expression<Func<int, int>> square2 = (int x) => { return x * x; };//не компилируется.
}

Пойдем дальше и разберем каждое лямбда-выражение поочередно. Первое лямбда-выражение является простым выражением, не имеющим тела оператора, так как нет оператора возврата и фигурных скобок, тогда как второе лямбда-выражение содержит тело оператора, так как имеет оператор возврата и фигурные скобки. Хотя оба выражения компилируются в делегат, преимущество лямбда-выражений без тела оператора заключается в том, что они могут быть преобразованы в дерево выражений, которое конкретный поставщик может использовать для генерации своей собственной реализации. Подобно LINQ для SQL, это преобразовывает дерево выражений в его язык предметной области, именуемый SQL, и отправляет его в базу данных. Третье лямбда-выражение показывает отличие лямбда-выражения от безымянного метода. Красота данного оператора заключается в том, что его легко превратить в выражение, тогда как безымянный метод можно превратить только в делегат. Прекрасно, что выражение можно превратить обратно в делегат путем компиляции выражения в делегат с помощью следующего синтаксиса:

Func<int,int> sq = squareexpr.Compile();

Последнее лямбда-выражение генерирует исключение, потому что компилятор не может преобразовать лямбда-выражение, содержащее тело оператора, о чем говорит то, что оно окружено фигурными скобками и оператором возврата.

Хотя можно использовать лямбда-выражения для генерации деревьев выражений, ничто не мешает вам прямо создать свое собственное дерево выражений. Разберем пример создания дерева выражения для лямбда-выражения square = x => x * x.

//пример создает дерево выражения x *x
    public static void CreatingExpressionTree()
    {
        ParameterExpression parameter1 = Expression.Parameter(typeof(int), "x");
        BinaryExpression multiply = Expression.Multiply(parameter1, parameter1);
        Expression<Func<int, int>> square = Expression.Lambda<Func<int, int>>(
            multiply, parameter1);
        Func<int, int> lambda = square.Compile();
        Console.WriteLine(lambda(5));
    }

Начнем с выражения параметра типа int.

ParameterExpression parameter1 = Expression.Parameter(typeof(int), "x");

Следующий шаг - создать тело лямбда-выражения, являющееся двоичным выражением. Тело состоит из оператора умножения для одного и того же выражения параметра.

BinaryExpression multiply = Expression.Multiply(parameter1, parameter1);

Заключительный шаг - создать лямбда-выражение, соединяющее тело с параметром следующим образом:

Expression<Func<int, int>> square = Expression.Lambda<Func<int, int>>(multiply,
    parameter1);

Последний шаг превращает выражение в делегат и выполняет делегат следующим образом:

Func<int, int> lambda = square.Compile();
Console.WriteLine(lambda(5));

Создание выражения из другого выражения

Можно взять дерево выражения и изменить его, чтобы создать другое выражение из него. В следующем примере начинаем с лямбда-выражения x *x, затем меняем это выражение, прибавляя к нему 2. Рассмотрим пример:

public static void CreatingAnExpressionFromAnotherExpression()
    {
        Expression<Func<int, int>> square = x => x * x;
        BinaryExpression squareplus2 = Expression.Add(square.Body,
            Expression.Constant(2));
        Expression<Func<int, int>> expr = Expression.Lambda<Func<int, int>>(squareplus2,
            square.Parameters);

        Func<int, int> compile = expr.Compile();
        Console.WriteLine(compile(10));
    }

Начинаем с лямбда-выражения, возвращающего square(квадрат):

Expression<Func<int, int>> square = x => x * x;

Затем генерируем тело нового лямбда-выражения, используя тело первого лямбда-выражения и прибавляя константу 2 к нему и назначая ее двоичному выражению:

BinaryExpression squareplus2 = Expression.Add(square.Body, Expression.Constant(2));

На последнем шаге генерируется новое лямбда-выражение путем соединения тела с параметрами из первого лямбда-выражения. В операторе ниже ссылка параметра должна точно совпадать со ссылкой из первого лямбда-выражения, являющегося square.Parameters. Нельзя создать новый экземпляр коллекции параметров, что дает ошибку при выполнении.

Expression<Func<int, int>> expr = Expression.Lambda<Func<int, int>>(squareplus2,
    square.Parameters);

Замыкания и лямбда-выражения

Замыкание – концепция, взятая из функционального программирования. Оно захватывает или использует переменную, находящуюся вне области видимости лямбда-выражения. Это значит, что вы можете использовать внутри лямбда-выражения переменные, объявленные вне области видимости лямбда-выражения — вы можете использовать и захватить переменную, находящуюся вне рамок лямбда-выражения. Это имеет свои плюсы, но может вызвать проблемы, так как внешний контекст может менять значение переменной. Разберем пример лямбда-выражения с учетом замыкания.

public static void LambdaWithClosure()
    {
        int mulitplyby = 2;
        Func<int, int> operation = x => x * mulitplyby;
        Console.WriteLine(operation(2));
    }

В примере выше используется переменная mulitplyby внутри лямбда-выражения, хотя она объявлена вне области видимости выражения. Такой принцип называется захватом переменной. На заднем плане компилятор C# берет все захваченные переменные и помещает их в сгенерированный класс. При использовании лямбда-выражений с внешними переменными, сборщик мусора не собирает их, и они существуют, пока не используются лямбда-выражениями и выражение не покинет область видимости.

Есть определенные ограничения при использовании лямбда-выражений с параметром с ключевым словом ref и out. Если переменная передается с ключевым словом ref или out, надо явно задать тип параметра, потому что компилятор не может вывести тип переменной. Как показано в примере ниже:

delegate void OutParameter(out int i);
    delegate void RefParameter(ref int i);
    public static void GotchasWithLambdas()
    {
        //пример с параметром out int i;
        OutParameter something = (out int x) => x = 5;
        something(out i);
        Console.WriteLine(i);

        //пример с параметром ref.
        int a = 2;
        RefParameter test = (ref int x) => x++;
        test(ref a);
        Console.WriteLine(a);
    }

Обратите внимание, что в коде выше явно задан тип параметра int в обоих случаях, ref и out. Если опустить тип параметра, компилятор выдаст ошибку.

Другое ограничение при использовании лямбд заключается в том, что нельзя использовать ключевое слово params в типе параметра для лямбда-выражения, независимо от того, задан явно тип параметра или нет. Следующий код не компилируется, потому что определение параметра описано ключевым словом params:

delegate void ParmsParameter(params int[] ints);
    public static void LambdaWithParam()
    {
        ParmsParameter par = (params int[] ints) =>
        {
            foreach (int i in ints)
            {
                Console.WriteLine(i);
            }
        };
    }

Вывод

В данной статье был рассмотрен синтаксис лямбда-выражения — как оно заменяет безымянный метод. Также сказано, что лямбда-выражения отличаются от безымянных методов из-за выведения типов и их способности легко превращаться в делегаты или деревья выражений. Были изучены ограничения параметров лямбда-выражений и то, как писать выражение с нуля и компилировать его в делегат, и наоборот.