1. Введение
Делегат — это тип, который описывает “подпись” функции: какие параметры принимает, что возвращает. Он как паспорт для функции или пропуск в ночной клуб для кода: если подпись совпадает — проходи, не совпадает — извини, тебя не пустят.
Пример:
public delegate int Operation(int a, int b);
Такой делегат обозначает функцию, принимающую два int и возвращающую int. Вот так выглядело программирование “до лямбд” (свежо, но не очень удобно):
Operation add = delegate (int x, int y)
{
return x + y;
};
С приходом лямбд код стал выглядеть лаконичнее:
Operation add = (x, y) => x + y;
Интересный факт: В C# лямбда-выражение — это просто способ создать “анонимную функцию”, которая автоматически компилируется в экземпляр делегата подходящего типа. А вот так связь между ними становится практически неразличимой!
Встроенные делегаты: Func, Action, Predicate
Чтобы не плодить бесконечно много собственных делегатов для каждого случая, в C# есть “универсальные” типы-функций:
- Func<T1, T2, ..., TResult> — универсальный делегат, принимающий параметры (T1, T2, ...) и возвращающий значение типа TResult.
- Action<T1, T2, ...> — почти то же самое, но ничего не возвращает (void).
- Predicate<T> — универсальный делегат, принимающий один параметр и возвращающий bool (любимец всех “фильтров”).
| Делегат | Возвращаемый тип | Пример синтаксиса |
|---|---|---|
|
|
|
|
|
|
|
|
|
Пример:
Func<int, int, int> add = (a, b) => a + b;
Action<string> show = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;
2. Лямбда-выражения “в конверте” делегата
Передача лямбды методу с делегатом
Любой метод, который принимает делегат, может “съесть” и лямбду, если её подпись совпадает с ожидаемой.
public static void CalculateAndShow(int a, int b, Func<int, int, int> operation)
{
int result = operation(a, b);
Console.WriteLine($"Результат: {result}");
}
// Вызываем с лямбдой
CalculateAndShow(5, 7, (x, y) => x * y); // Результат: 35
Пример с Action
void ProcessString(string message, Action<string> processor)
{
processor(message);
}
// Используем лямбду
ProcessString("Привет, мир!", s => Console.WriteLine(s.ToUpper()));
Пример с Predicate
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
Вот в такие моменты и понимаешь: зачем выдумывать самописные делегаты, если есть проверенное "колесо" от Microsoft.
Как работают делегаты и лямбды вместе
flowchart TD
A[Лямбда-выражение] -->|Создаёт| B((Делегат))
B -->|Передаётся как аргумент| C[Метод]
B -->|Хранится| D[Свойство/Поле/Коллекция]
C -->|Вызывает| E[Код лямбды]
3. Передача лямбды как делегата в собственных методах
Если ваш собственный метод использует делегаты, лямбда делает использование максимально гибким.
Пример: наш мини-калькулятор
// Определяем метод для работы с операциями
static int PerformOperation(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}
// Применяем разные операции (лямбды - в деле!)
int sum = PerformOperation(3, 4, (x, y) => x + y);
int multiply = PerformOperation(3, 4, (x, y) => x * y);
int max = PerformOperation(3, 4, (x, y) => x > y ? x : y);
Console.WriteLine(sum); // 7
Console.WriteLine(multiply); // 12
Console.WriteLine(max); // 4
Видите, как элегантно меняется логика, вместо трёх отдельных методов?
4. Лямбды + делегаты в LINQ (и не только)
В LINQ методы ожидают “логическую мини-функцию”, то есть как раз делегат. Почти все стандартные LINQ-методы принимают такие типы:
- Func<T, bool> для фильтрации (Where)
- Func<T, TResult> для трансформаций (Select)
- Func<T, TKey> для сортировок (OrderBy)
List<string> names = new List<string> { "Анна", "Олег", "Виктор", "Яна" };
// Получаем только имена длиннее 3-х символов
var longNames = names.Where(name => name.Length > 3);
// Преобразуем имена в верхний регистр
var upperNames = names.Select(name => name.ToUpper());
LINQ — это “лямбды на максималках”, потому что в них всё, что можно передать как делегат, можно передать как лямбду!
5. Комбинирование: хранение и передача делегатов-лямбд
Список функций
Лямбда — это не просто временный анонимный метод. Вы можете хранить набор лямбд в коллекции и использовать их динамически.
List<Func<int, int, int>> operations = new List<Func<int, int, int>>
{
(x, y) => x + y,
(x, y) => x - y,
(x, y) => x * y,
(x, y) => x / y
};
foreach (var op in operations)
{
Console.WriteLine(op(10, 2));
}
Такой приём часто встречается в тестах, обработчиках и плагинах.
Словарь делегатов
А теперь, если вы фанат plug-and-play архитектуры:
var mathFuncs = new Dictionary<string, Func<int, int, int>>
{
{ "plus", (x, y) => x + y },
{ "minus", (x, y) => x - y },
{ "pow", (x, y) => (int)Math.Pow(x, y) }
};
string command = "pow"; // Имитация пользовательского ввода
if (mathFuncs.TryGetValue(command, out var operation))
{
Console.WriteLine(operation(2, 5)); // 32
}
else
{
Console.WriteLine("Команда не найдена");
}
6. Полезные нюансы
Лямбда-делегаты: возврат делегата из метода
Можно возвращать делегаты (а значит, и лямбды) из методов — получается почти фабрика функций!
Func<int, int> GetMultiplier(int factor)
{
// Возвращаем лямбда-делегат, который будет умножать на factor
return x => x * factor;
}
var triple = GetMultiplier(3);
Console.WriteLine(triple(5)); // 15
var quadruple = GetMultiplier(4);
Console.WriteLine(quadruple(5)); // 20
Вот так замыкание (closure) становится не игрушкой, а реальным инструментом!
Составление функций: Combine, Delegate.Combine и мультиделегаты
Иногда хочется, чтобы один “вызов” привёл к выполнению сразу нескольких функций. Например, обработка событий в GUI: жмём на кнопку — и сразу несколько обработчиков реагируют.
Action и делегаты позволяют “состыковывать” несколько функций:
Action<string> pipeline = s => Console.WriteLine("Шаг 1: " + s);
pipeline += s => Console.WriteLine("Шаг 2: " + s.ToUpper());
pipeline += s => Console.WriteLine("Шаг 3: " + s.Length);
pipeline("тест");
// Выводит:
// Шаг 1: тест
// Шаг 2: ТЕСТ
// Шаг 3: 4
Удобно для сценариев “цепочек” или обработки событий.
Важно: Для функций, возвращающих значения, результат будет только от последнего вызова (“хвост функции”). Для Action — выполняются все, так как void.
Использование лямбда-делегатов как параметры callback
Классический сценарий — “вызови меня, когда закончишь”.
void DownloadFile(string url, Action<string> onFinish)
{
// Имитация загрузки файла...
System.Threading.Thread.Sleep(500);// (Имитация долгой работы, не используйте в реальных GUI-приложениях)
onFinish($"Загрузка '{url}' завершена!");
}
DownloadFile("http://example.com", msg => Console.WriteLine(msg));
В реальных приложениях (например, при работе с API, базами, файлами) — это стандартный паттерн.
Функция высшего порядка: пример фильтра по предикату
Лямбда-выражения и делегаты позволяют писать универсальные функции, которые принимают другие функции как параметры. Это основа шаблона “функции высшего порядка”.
List<int> Filter(List<int> source, Predicate<int> condition)
{
List<int> result = new List<int>();
foreach (var item in source)
if (condition(item)) result.Add(item);
return result;
}
// Используем разные лямбды:
var onlyPositive = Filter(new List<int> { -2, 0, 2, 7 }, x => x > 0);
var onlyEven = Filter(new List<int> { -2, 0, 2, 7 }, x => x % 2 == 0);
Можно даже создавать “фабрики” лямбд:
Func<int, Predicate<int>> GetRangeChecker(int min, int max) =>
x => x >= min && x <= max;
Predicate<int> inRange = GetRangeChecker(1, 10);
var filtered = Filter(new List<int> { 0, 5, 10, 15 }, inRange); // 5, 10
7. Типичные ошибки и нюансы комбинирования
Если подпись параметров не совпадает, компилятор будет бурчать (CS1660: "Cannot convert lambda expression").
Лямбда-выражение может неожиданно “запоминать” переменные, которые позже меняются (замыкание / closure) — будьте внимательны к захвату переменных.
Мультиделегаты с возвращаемым значением возвращают результат только от последнего обработчика — часто неожиданность для начинающих.
В местах, где нужны выражения Expression<Func<int,bool>>, нельзя подставить statement-лямбду — требуется именно expression-лямбда.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ