1. Введение
Hãy tưởng tượng bạn có một máy pha cà phê. Thường thì nó chỉ pha cà phê và không làm phiền ai, nhưng đôi khi sếp nói: "Bạn có thể, làm ơn, sau khi pha xong hét 'Xong!'" — và ở đây cần cấu hình. Chúng ta không sửa máy pha. Chỉ cần tạo một hàm mà nó sẽ gọi ở cuối.
Trong C# vai trò này được thực hiện bởi các delegate — chúng cho phép truyền vào phương thức những đoạn mã (hàm, lambda hoặc anonymous method), để phương thức đó gọi chúng khi cần. Nói gọn, delegate là một kiểu có thể giữ tham chiếu tới các phương thức có chữ ký xác định.
Определение делегата
Trong C# delegate được định nghĩa bằng từ khóa delegate. Ví dụ:
// Delegate tham chiếu tới các phương thức nhận int và trả về bool
public delegate bool PredicateInt(int x);
Bây giờ biến kiểu PredicateInt có thể tham chiếu tới bất kỳ phương thức (hoặc lambda!) nào nhận một int và trả về bool.
Для чего нужны делегаты?
- Truyền logic như một tham số (ví dụ, cho sorting, filtering, xử lý sự kiện)
- Đăng ký sự kiện (sẽ nói sau)
- Triển khai callback
- API linh hoạt, nơi phần hành vi được định nghĩa bởi bên gọi
Простая визуальная схема
| Тип делегата | Сигнатура | Пример вызова |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. Как лямбда превращается в делегат
Синтаксис
Khi bạn viết một biểu thức lambda, ví dụ x => x > 5, về cơ bản bạn đang tạo một đối tượng delegate. Lambda không "sống" riêng lẻ: nó cần một kiểu (ai đó phải biết tập tham số và kiểu trả về). Đó là lý do biểu thức lambda trong C# luôn được chuyển (ngầm hoặc rõ ràng) thành một delegate.
Пример 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<...>
Dùng cho các phương thức nhận tham số và trả về giá trị.
Сигнатура:
— Kiểu cuối cùng là kiểu trả về, các kiểu trước đó là kiểu các tham số, ví dụ:
Func<int, string> — nhận int, trả về string
Func
intToString = number => "Число: " + number; Console.WriteLine(intToString(7)); // "Число: 7"
Action<...>
Dùng khi cần thực thi một việc nào đó nhưng không trả về gì (void).
Action
printHello = name => Console.WriteLine("Привет, " + name + "!"); printHello("Василий"); // "Привет, Василий!"
Predicate<T>
Về cơ bản là viết tắt của Func<T, bool>. Dùng khi cần kiểm tra logic trên một đối tượng (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# --> Объект delegate (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).
GO TO FULL VERSION