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 (точнее, ничего явно не возвращает). Будьте внимательны к возвращаемому типу!
Ошибка: злоупотребление лямбдами
Если вы начинаете передавать лямбды из 10 строк, лучше вынесите их в отдельный метод. Это и читабельнее, и отладка проще.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ