1. Введение
Представьте, что у вас есть кофемашина. Обычно она просто варит кофе и никого не трогает, но иногда шеф говорит: «А можешь, пожалуйста, после варки кофе покричать “Готово!”?» — и здесь нужна настройка. Саму кофемашину мы не переписываем. Просто создаём функцию, которую она выполнит в конце.
В C# эту роль выполняют делегаты — они позволяют передавать в методы куски кода (методы, лямбды или анонимные методы), чтобы тот метод вызвал их в нужный момент. Грубо говоря, делегат — это тип, который может хранить ссылки на методы с определённой сигнатурой.
Определение делегата
В C# делегат определяется с помощью ключевого слова delegate. Пример:
// Делегат, который ссылается на методы, принимающие int и возвращающие bool
public delegate bool PredicateInt(int x);
Теперь переменная типа PredicateInt сможет ссылаться на любой метод (или лямбду!), который принимает один int и возвращает bool.
Для чего нужны делегаты?
- Передача логики как аргумента (например, для сортировки, фильтрации, обработки событий)
- Подписка на события (о них мы поговорим позже)
- Реализация обратных вызовов (callback)
- Гибкие API, где часть поведения определяется вызывающей стороной
Простая визуальная схема
| Тип делегата | Сигнатура | Пример вызова |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. Как лямбда превращается в делегат
Синтаксис
Когда вы пишете лямбда-выражение, например, x => x > 5, вы, по сути, создаёте объект делегата. Лямбда не «живет» в вакууме: ей обязательно нужен тип (кто-то должен знать, какой у неё набор параметров и возвращаемый результат). Вот почему лямбда-выражение в C# всегда неявно (или явно) преобразуется в делегат.
Пример 1: Связывание делегата с методом
// Явно определяем делегат
public delegate bool MyPredicate(int number);
class Program
{
static void Main()
{
// Присваиваем лямбду переменной типа MyPredicate
MyPredicate isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // true
Console.WriteLine(isEven(7)); // false
}
}
Пример 2: Использование стандартных делегатов
C# содержит набор стандартных обобщённых делегатов: Action, Func<>, Predicate<>. Они используются почти везде, где вы пишете лямбды в коде.
// Используем Func
Func
isPositive = number => number > 0; Console.WriteLine(isPositive(-5)); // false
3. Стандартные делегаты: Func, Action, Predicate
Func<...>
Используется для методов, которые что-то принимают и что-то возвращают.
Сигнатура:
— Последний тип — возвращаемое значение, остальные до него — типы параметров, например:
Func<int, string> — принимает int, возвращает string
Func
intToString = number => "Число: " + number; Console.WriteLine(intToString(7)); // "Число: 7"
Action<...>
Используется, если надо что-то выполнить, но возвращать ничего не надо (void).
Action
printHello = name => Console.WriteLine("Привет, " + name + "!"); printHello("Василий"); // "Привет, Василий!"
Predicate<T>
По сути, сокращение для Func<T, bool>. Используется, когда требуется логическая проверка над объектом (true/false).
Predicate
isOdd = x => x % 2 != 0; Console.WriteLine(isOdd(3)); // true
Визуальная шпаргалка
| Делегат | Сигнатура | Применение |
|---|---|---|
|
|
Преобразование, проекция |
|
|
Сайд-эффекты, вывод |
|
|
Фильтрация, поиск |
Какой тип делегата выбрать для лямбда-выражения?
- Если ожидается возвращаемое значение, возьмите Func<...>
- Если метод ничего не возвращает (void), используйте Action<...>
- Если нужна проверка условия, используйте Predicate<T>
Пример: Фильтрация списка
List
numbers = new List
{ 1, 2, 3, 4, 5, 6 }; // Ожидает Predicate
List
evenNumbers = numbers.FindAll(x => x % 2 == 0); Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
4. Полезные нюансы
Лямбда-выражения и методы коллекций: что под капотом?
Когда вы вызываете метод коллекции, передавая лямбду, например:
var adults = users.Where(u => u.Age >= 18);
Метод Where ожидает аргумент типа Func<T, bool>. То есть, ваша лямбда u => u.Age >= 18 превращается компилятором в объект делегата такого типа.
Блок-схема: как это работает
Ваша лямбда --> Компилятор C# --> Объект делегата (Func
) (u => u.Age >= 18) [тип известен] (Готов к вызову в Where())
Детальнее о типизации: вывод типа
Обычно тип делегата выводится компилятором автоматически, если он ясен из контекста. Например, для метода List<T>.Find ожидается Predicate<T>, и компилятор знает тип параметра по сигнатуре метода.
List
words = new List
{ "one", "two", "three" }; var result = words.Find(word => word.Length == 5); // Find ожидает Predicate
Console.WriteLine(result); // "three"
Если же контекст не ясен, придётся помочь компилятору:
// Явно указываем тип
Func
check = x => x > 10;
Возвращаемые делегаты: фабрика функций
Иногда методы могут возвращать делегаты — то есть создавать «фабрики функций». Это удобно для генерации динамического поведения.
// Функция, возвращающая делегат (лямбду)
Func
GetMultiplier(int factor) { return x => x * factor; } var times3 = GetMultiplier(3); Console.WriteLine(times3(5)); // 15
Это работает, потому что лямбда (x => x * factor) захватывает переменную factor из внешнего контекста (closure/замыкание) и возвращается как объект типа Func<int, int>.
5. Ошибки и недоразумения с делегатами и лямбдами
Несовпадение сигнатур
Компилятор не даст записать лямбду в делегат, если параметры или возвращаемое значение не совпадают.
Func
f = x => "Нельзя вернуть строку!"; // Ошибка компиляции
Ошибка при попытке использовать лямбду без делегата
Нельзя просто написать лямбду и попробовать её вызвать без типа:
// Это не сработает - компилятор не может вывести тип
// var myFunc = x => x * 2; // Ошибка CS0815
// myFunc(10);
Чтобы работало, нужно явно указать тип, или предоставить контекст:
Func
myFunc = x => x * 2; Console.WriteLine(myFunc(10)); // 20
Путаница между Action, Func и Predicate
Иногда можно случайно выбрать не тот тип делегата, из-за чего будет ошибка соответствия сигнатуры. Помните простое правило: Func — когда есть результат, Action — когда результата нет (void), Predicate<T> — когда нужен логический ответ (bool).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ