1. Вступ
Досі ви бачили: ми викликали методи LINQ, які приймали на вхід лямбду. Але як метод C# розуміє, що йому взагалі можна передати функцію?
Простими словами, делегати — це своєрідний інтерфейс для функцій: ми описуємо сигнатуру (типи параметрів і значення, що повертається), і будь‑який метод (і лямбда‑вираз!), що відповідає цій сигнатурі, можна передати туди, де очікується такий делегат.
Згадайте, як ви передавали рядок як параметр. Приблизно так само працює і з «логікою», лише тип параметра — делегат.
Делегати: базова теорія по‑людськи
У C# делегат — це тип, що описує «функцію з такою‑то сигнатурою».
// Делегат, який приймає int і повертає bool
public delegate bool IntPredicate(int x);
Будь‑яку функцію, сумісну за сигнатурою, можна присвоїти змінній цього типу:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
А тепер підійде й лямбда‑вираз:
IntPredicate pred = x => x % 2 == 0;
Універсальні делегати: Func, Action, Predicate
- Func<T1, ..., TResult> — функція, яка приймає параметри T1, ... і повертає TResult.
- Action<T1, ...> — функція, яка приймає параметри і не повертає значення (void).
- Predicate<T> — функція, яка приймає T і повертає bool.
2. Передавання лямбди у власний метод
Уявімо, що ми розвиваємо наш навчальний міні‑застосунок — консольний проєкт, який працює зі списком користувачів. Раніше ми фільтрували колекції через LINQ, а тепер напишемо власний метод, що приймає лямбду‑умову.
Створюємо власний метод із лямбдою‑параметром
// Визначимо клас User для прикладу (додамо в наш застосунок)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// Метод, який приймає список і делегат‑умову (лямбду)
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // Викликаємо лямбду!
result.Add(user);
}
return result;
}
Тепер можна передавати будь‑яку лямбду:
var users = new List<User>
{
new User { Name = "Василь", IsActive = true },
new User { Name = "Петро", IsActive = false },
new User { Name = "Марія", IsActive = true }
};
// Фільтруємо лише активних
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // Василь, Марія
Ось і все! Ми передали шмат логіки — міні‑функцію — як звичайний параметр, бо метод FilterUsers очікує Predicate<User>, а ми надали йому відповідну лямбду.
Варіант із Func<T, TResult>
Predicate<T> доречний, коли потрібна умова (повертається bool). А якщо хочемо щось «обчислити» для кожного користувача?
// Метод, який застосовує функцію до кожного елемента і збирає результати
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
var result = new List<TResult>();
foreach (var user in users)
{
result.Add(selector(user));
}
return result;
}
Використання:
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // ВАСИЛЬ, ПЕТРО, МАРІЯ
3. Корисні нюанси
Різні способи передавання
Можна передати не лише лямбду, а й звичайний метод — головне, щоб сигнатура збігалася.
// Звичайний метод
static bool NameHasS(User user) => user.Name.Contains("с");
// Передавання звичайного методу:
var usersWithS = FilterUsers(users, NameHasS);
// Передавання лямбди:
var usersWithA = FilterUsers(users, u => u.Name.Contains("а"));
А можна й анонімний метод старого зразка (але краще так не робити):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("м"); });
Сучасний стиль — лямбди!
Передавання лямбд у LINQ: що насправді відбувається
var result = users.Where(u => u.IsActive).ToList();
Під капотом Where приймає Func<User, bool>. Це означає, що будь‑який метод, який приймає Func<...>, можна використовувати так само!
А що, якщо хочеться два параметри?
// Метод приймає дві лямбди для фільтрації
public static List<User> FilterUsersCustom(
List<User> users,
Func<User, bool> include,
Func<User, bool> exclude)
{
var result = new List<User>();
foreach (var user in users)
{
if (include(user) && !exclude(user))
result.Add(user);
}
return result;
}
Використання:
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("В"),
u => u.IsActive == false
);
// Візьме лише користувачів, чиї імена починаються на "В", і тих, хто активний
Сценарій: Фабрика фільтрів
Console.WriteLine("Введіть мінімальну довжину імені:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// Досить інтерактивно й жваво!
4. Типові помилки та нюанси
Іноді компілятор не може «вивести» тип параметрів лямбди — особливо в складних сценаріях із перевантаженнями або коли метод вимагає делегата з кількома параметрами чи з конкретним типом, що повертається. У такому разі типи лямбди можна вказати явно:
FilterUsers(users, (User u) => u.Name.Length > 3);
Або навіть так:
MapUsers(users, (User u) => u.Name.ToUpper());
Помилка: лямбда не підходить за сигнатурою
FilterUsers(users, user => Console.WriteLine(user.Name)); // Помилка! Очікується bool, отримано void
Адже очікується функція, що повертає bool, а лямбда повертає void (точніше, нічого явно не повертає). Будьте уважні до типу, який повертається!
Помилка: зловживання лямбдами
Якщо ви починаєте передавати лямбди на десять рядків, краще винесіть їх у окремий метод. Так і читабельніше, і налагоджувати простіше.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ