Изучение лямбда-выражений в 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));