Лямбда-выражение это небольшая анонимная функция, записанная прямо на месте с оператором => («переходит в»). Параметры слева, результат справа:
Вывод:
60.0
12
ORDER SHIPPED!
96.0, 180.0
У лямбды нет собственного типа. Она становится делегатом (объектом, указывающим на функцию) того типа, который ожидает контекст: здесь Func<decimal, decimal>, для List.FindAll это Predicate<T>, для события EventHandler. Делегаты Func возвращают значение, а делегаты Action нет; полное семейство перечислено на странице о Func и Action.
Формы синтаксиса
x => x * x // one parameter: parentheses optional
(x, y) => x + y // several parameters
() => DateTime.Now.Year // no parameters
(int x, string s) => s.Length > x // explicit parameter types
order => // statement lambda: a block with return
{
decimal subtotal = order.Qty * order.Price;
return subtotal > 100m ? subtotal * 0.9m : subtotal;
}
Лямбда-выражение содержит одно выражение после =>, и его значение является результатом. Лямбда с телом-блоком (statement lambda) содержит блок в фигурных скобках, в котором могут быть любые операторы, и она должна возвращать значение через return на каждом пути, если делегат возвращает значение (иначе CS1643, Not all code paths return a value in lambda expression).
Новые версии добавили другие формы, все необязательные:
- C# 9: параметры-отбрасывания, так что несколько неиспользуемых параметров могут все называться
_((_, _) => Save()), и лямбдыstatic, которым запрещено захватывать переменные. - C# 10: «естественный тип», поэтому
var square = (int x) => x * x;выводитFunc<int, int>. До этогоvar f = x => x * 2;не компилируется; даже в C# 10 без типа параметра по-прежнему ошибка CS8917The delegate type could not be inferred. - C# 12: значения параметров по умолчанию,
(int x, int y = 1) => x + y.
Передача лямбд в методы
Любой метод с параметром-делегатом принимает лямбду. Так передают поведение, а не только данные:
Вывод:
report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf
Filter не знает, что значит «оставить»; это решает вызывающий код с помощью лямбды. List.Sort работает так же: лямбда это сравнение, которое возвращает отрицательное число, ноль или положительное, а Sort вызывает её столько раз, сколько нужно.
Лямбды в LINQ
В LINQ большая часть кода на C# и встречается с лямбдами. Каждый оператор запроса принимает лямбду: Where условие, Select проекцию, OrderBy ключ, Sum суммируемое значение:
Вывод:
Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45
Лямбды не выполняются там, где написаны. LINQ сохраняет их и вызывает, когда запрос перечисляется через foreach, ToList() или Sum(); это называется отложенным выполнением. Сами операторы описаны на странице о LINQ.
Замыкания: захваченные переменные
Лямбда может использовать локальные переменные и параметры метода, в котором она написана. Она захватывает переменную, а не её текущее значение, и переменная живёт, пока живёт лямбда:
Вывод:
1
2
3
1
50.0
Каждый вызов MakeCounter создаёт новую count, поэтому два счётчика независимы. Лямбда скидки прочитала rate в момент выполнения, а не в момент написания, поэтому применила 50%. Компилятор реализует это, перенося захваченные переменные в скрытый класс, общий для метода и лямбды.
Ловушка захвата в цикле
Захват переменной цикла это место, где замыкания кусаются. У цикла for одна переменная на весь цикл, поэтому все созданные в нём лямбды разделяют эту переменную:
Вывод:
3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach
Все три лямбды в первом цикле выполнились после окончания цикла, когда i было равно 3. Копирование i в переменную, объявленную внутри тела, даёт каждой лямбде свою. foreach сам делает это правильно: начиная с C# 5 его переменная итерации это новая переменная на каждом проходе. До C# 5 у него была та же ошибка, поэтому старые ответы в интернете до сих пор советуют копию и для foreach.
Та же ловушка касается всего, что выполняется позже: обработчиков событий, таймеров, Task.Run и запросов LINQ, построенных в цикле и перечисляемых после него.
Лямбды, анонимные методы и локальные функции
Лямбды заменили синтаксис анонимных методов из C# 2, который по-прежнему компилируется:
Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; }; // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0; // lambda
Для вспомогательной функции, используемой только внутри одного метода, C# 7.0 также предлагает локальные функции, у которых есть имя, которые могут быть рекурсивными и не создают делегат, если вы его явно не создаёте. Используйте лямбду, когда передаёте поведение чему-то (LINQ, Sort, событию); используйте локальную функцию или закрытый метод, когда вызываете её сами.
Ещё одно различие проявляется у поставщиков LINQ вроде Entity Framework: лямбда, присвоенная Expression<Func<T, bool>>, вообще не компилируется в код, а превращается в структуру данных, описывающую код, которую поставщик переводит в SQL. Синтаксис одинаковый; какой вариант вы получите, решает тип параметра вызываемого метода.
Часто задаваемые вопросы
Что такое лямбда-выражение в C#?
Анонимная функция, записанная с оператором =>: x => x * 2 принимает x и возвращает x * 2. У лямбды нет собственного имени; она преобразуется в тип делегата, например Func<int, int> или Action<string>, и её можно сохранить в переменной, передать в метод или вернуть из него.
Как написать лямбду с несколькими параметрами или без параметров?
Заключите параметры в скобки: (a, b) => a + b для двух, () => DateTime.Now для нуля. Скобки можно опустить только для одного параметра (x => x + 1). Можно также указать типы явно: (int a, int b) => a + b.
Что такое замыкание в лямбде C#?
Лямбда, использующая локальную переменную окружающего метода, захватывает саму переменную, а не снимок её значения. Переменная живёт столько же, сколько лямбда, и если какая-то сторона потом её изменит, другая увидит изменение. Именно на этом работают счётчики и обратные вызовы, и именно это вызывает классическую ошибку захвата в цикле for.
Почему все лямбды в цикле for видят одно и то же значение?
У цикла for одна переменная цикла на весь цикл, поэтому каждая созданная внутри лямбда захватывает одну и ту же i и видит её итоговое значение, когда выполняется позже. Скопируйте её в локальную переменную внутри тела (int copy = i;) и захватывайте копию. У foreach этой проблемы нет начиная с C# 5, потому что он создаёт новую переменную на каждой итерации.
Почему var f = x => x * 2 не компилируется?
Компилятор не может определить, какой тип делегата вам нужен и какого типа x, поэтому сообщает CS8917 The delegate type could not be inferred (до C# 10 CS0815). Объявите тип (Func<int, int> f = x => x * 2;) или в C# 10 и новее укажите тип параметра: var f = (int x) => x * 2;.