Лямбда-выражения в C# с примерами
Лямбда-выражение в C# - это короткая запись анонимной функции, которую можно сохранить в переменную, передать в метод или вернуть как результат. Оператор => отделяет параметры от тела, а сама лямбда превращается либо в делегат, либо в дерево выражения. Ниже разобраны синтаксис, замыкания, работа с LINQ, типы Func и Action, статические лямбды и ошибки, которые встречаются чаще всего.

Содержание
Что такое лямбда-выражение и как оно записывается
Лямбда - это функция без имени. Слева от оператора => стоят параметры, справа - выражение или блок инструкций. Компилятор сам превращает такую запись в делегат подходящего типа, поэтому писать отдельный метод ради одноразовой логики не нужно.
Есть две формы тела. Выражение пишется без фигурных скобок и без return, его значение и становится результатом. Блок в фигурных скобках позволяет уместить несколько инструкций и обязан содержать return, если лямбда возвращает значение.
Func<int, int> square = x => x * x; // выражение
Func<int, int, int> sum = (a, b) => a + b; // два параметра
Func<int, string> describe = n => // блок
{
if (n < 0) return "отрицательное";
return n == 0 ? "ноль" : "положительное";
};
Console.WriteLine(square(7)); // 49
Console.WriteLine(sum(3, 4)); // 7
Console.WriteLine(describe(-5)); // отрицательное
Скобки вокруг параметра обязательны, когда параметров ноль или больше одного: () => 42, (a, b) => a + b. Для одного параметра без указания типа скобки можно опустить.
Типы параметров и результат
Обычно компилятор выводит типы параметров из контекста, но их можно указать явно: (int a, int b) => a + b. Явные типы нужны, когда контекста недостаточно, например при записи лямбды в переменную через var. Начиная с C# 10 у лямбды есть естественный тип, поэтому такая запись допустима.
var add = (int a, int b) => a + b; // Func<int, int, int>
var greet = (string name) => $"Привет, {name}!";
var log = (string text) => Console.WriteLine(text); // Action<string>
Console.WriteLine(add(2, 5));
Console.WriteLine(greet("Анна"));
log("готово");
Также в C# 10 появилась возможность указывать тип возвращаемого значения перед параметрами, а в C# 12 - значения параметров по умолчанию: var inc = (int x, int step = 1) => x + step;. Эти возможности нужны редко, но полезны, когда компилятор не может вывести тип самостоятельно.
Func, Action и Predicate: на что превращается лямбда
Лямбда не существует сама по себе, она всегда преобразуется в конкретный тип делегата. В библиотеке для этого есть готовые универсальные типы, и писать собственный delegate приходится нечасто. Подробнее о передаче функции в метод рассказано в статье про функцию как параметр метода.
| Тип | Что принимает | Что возвращает | Пример |
|---|---|---|---|
Func<T, TResult> | до 16 параметров | значение типа TResult | x => x * 2 |
Action<T> | до 16 параметров | ничего (void) | s => Console.WriteLine(s) |
Predicate<T> | один параметр | bool | n => n > 10 |
Comparison<T> | два параметра типа T | int | (a, b) => a.CompareTo(b) |
В Func последний типовой параметр всегда означает результат, остальные - аргументы. Поэтому Func<int, int, bool> принимает два числа и возвращает логическое значение. Action используют, когда важен побочный эффект: вывод, запись в журнал, изменение состояния.
var numbers = new List<int> { 5, 12, 8, 21, 3 };
Predicate<int> isBig = n => n > 10;
int firstBig = numbers.Find(isBig); // 12
numbers.ForEach(n => Console.Write(n + " ")); // Action
numbers.Sort((a, b) => b.CompareTo(a)); // Comparison, по убыванию
Console.WriteLine(string.Join(", ", numbers));
Лямбды и LINQ
Самое частое применение лямбд - методы LINQ. Практически каждый из них принимает Func или Expression<Func>: условие для Where, ключ для OrderBy, преобразование для Select. Запись получается короткой и читается слева направо как описание того, что нужно получить.
var products = new List<Product>
{
new("Мышь", "Периферия", 1200m),
new("Клавиатура", "Периферия", 3400m),
new("Монитор", "Экраны", 15900m),
new("Кабель HDMI", "Аксессуары", 450m),
};
var result = products
.Where(p => p.Price > 1000m)
.OrderByDescending(p => p.Price)
.Select(p => $"{p.Name}: {p.Price:N0} руб.")
.ToList();
var byCategory = products
.GroupBy(p => p.Category)
.Select(g => new { Category = g.Key, Total = g.Sum(p => p.Price) });
foreach (var item in byCategory)
Console.WriteLine($"{item.Category}: {item.Total}");
record Product(string Name, string Category, decimal Price);
Если нужно отсортировать коллекцию по свойству или найти элемент с наибольшим значением, лямбда задаёт критерий. Готовые рецепты есть в статьях про сортировку List по свойству и поиск минимума и максимума в LINQ.
Func и Expression: в чём разница
Для обычных коллекций (IEnumerable<T>) лямбда становится делегатом и выполняется в памяти. Для источников вроде Entity Framework (IQueryable<T>) метод ждёт Expression<Func<T, bool>>. Это дерево выражения, которое провайдер превращает в SQL. Внутри такой лямбды можно использовать только то, что провайдер умеет переводить, а вызов собственных методов часто приводит к исключению.
using System.Linq.Expressions;
Expression<Func<int, bool>> expr = n => n > 5;
Console.WriteLine(expr); // n => (n > 5)
Func<int, bool> compiled = expr.Compile();
Console.WriteLine(compiled(10)); // True
Замыкания: захват переменных
Лямбда может использовать локальные переменные метода, в котором объявлена. Это называется замыканием. Компилятор выносит такие переменные в скрытый класс, поэтому лямбда помнит значения даже после выхода из метода.
static Func<int> CreateCounter()
{
int count = 0;
return () => ++count; // count живёт, пока жива лямбда
}
var c1 = CreateCounter();
var c2 = CreateCounter();
Console.WriteLine(c1()); // 1
Console.WriteLine(c1()); // 2
Console.WriteLine(c2()); // 1, у каждого счётчика своя переменная
Захватывается именно переменная, а не её значение на момент создания лямбды. Если после создания переменную изменить, лямбда увидит новое значение. Это поведение удобно для счётчиков и накопителей, но является источником неожиданных ошибок.
int factor = 2;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(10)); // 20
factor = 5;
Console.WriteLine(multiply(10)); // 50, лямбда видит новое значение
Захват переменной цикла
В цикле foreach каждая итерация получает новую переменную, поэтому лямбды запоминают разные значения. В цикле for переменная счётчика одна на весь цикл, и все лямбды увидят её итоговое значение. Решение простое: скопировать значение в локальную переменную внутри тела цикла.
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i; // без копии вывод был бы 3 3 3
actions.Add(() => Console.Write(copy + " "));
}
actions.ForEach(a => a()); // 0 1 2
Статические лямбды
Модификатор static перед лямбдой (C# 9) запрещает ей захватывать локальные переменные и this. Такая лямбда не создаёт скрытый объект замыкания и не выделяет память при каждом вызове, а компилятор может кешировать делегат. Модификатор также защищает от случайного захвата: если внутри использована внешняя переменная, код не скомпилируется.
int offset = 100;
Func<int, int> pure = static x => x * 2; // можно
// Func<int, int> bad = static x => x + offset; // ошибка: захват запрещён
var doubled = new[] { 1, 2, 3 }.Select(static n => n * 2);
Применяйте static в горячих участках кода и в библиотеках, где важно отсутствие лишних аллокаций. В обычном прикладном коде выигрыш мал, но привычка помогает не захватывать лишнего.
Типичные ошибки при работе с лямбдами
- Захват переменной цикла
forбез копии, из-за чего все лямбды работают с одним значением. - Отложенное выполнение LINQ: запрос с лямбдой выполняется в момент перебора, а не при объявлении, и видит актуальные значения захваченных переменных.
- Утечки памяти при подписке на событие лямбдой: отписаться от анонимной лямбды нельзя, пока не сохранена ссылка на делегат.
- Слишком длинное тело. Если в лямбде больше пяти-семи строк, её лучше вынести в именованный метод.
- Вызов собственных методов внутри
Expressionпри работе с базой данных, который провайдер не может перевести в SQL. - Изменение коллекции внутри лямбды, которая выполняется во время перебора этой же коллекции.
Про событие и отписку: если нужно позже убрать обработчик, сохраните лямбду в переменную и используйте её же в операции отписки.
EventHandler handler = (s, e) => Console.WriteLine("клик");
button.Click += handler;
// позже
button.Click -= handler;
Лямбда или локальная функция
Локальная функция объявляется внутри метода обычным синтаксисом и подходит, когда нужны имя, рекурсия или атрибуты. Лямбда удобнее для коротких одноразовых выражений, особенно как аргумент метода. Локальные функции не создают делегат, пока вы не передали их как значение, поэтому для вспомогательной логики внутри метода они немного экономнее.
static int SumOfSquares(IEnumerable<int> items)
{
return items.Sum(Square);
static int Square(int x) => x * x; // локальная статическая функция
}
Частые вопросы
Чем лямбда отличается от анонимного метода?
Анонимный метод пишется через ключевое слово delegate и громоздок. Лямбда короче, умеет выводить типы параметров и может превращаться в дерево выражения. В новом коде анонимные методы почти не используют.
Можно ли вернуть значение из лямбды типа Action?
Нет, Action возвращает void. Если нужен результат, используйте Func. Исключение: однострочная лямбда с вызовом метода допустима для Action, даже если метод что-то возвращает, результат просто отбрасывается.
Как сделать рекурсивную лямбду?
Сначала объявите переменную с явным типом делегата, затем присвойте ей лямбду: Func<int, int> fact = null!; fact = n => n <= 1 ? 1 : n * fact(n - 1);. Для сложной рекурсии лучше локальная функция.
Замедляют ли лямбды программу?
Вызов делегата чуть дороже прямого вызова метода, а замыкание создаёт объект в куче. В большинстве задач разница незаметна. В критичных участках применяйте статические лямбды или обычные методы.
Почему лямбда в Entity Framework выбрасывает исключение?
Провайдер переводит дерево выражения в SQL и не знает ваших методов. Вычислите значение заранее в переменную или выполните часть запроса в памяти после ToList().
Можно ли использовать ref и out в лямбдах?
Параметры лямбды могут быть ref, out и in, но внешние ref-параметры метода внутри лямбды использовать нельзя. Для такой логики подойдёт локальная функция.





