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