C#JuniorCoding
Что такое делегаты Action, Func и Predicate?
Action — делегат без возвращаемого значения, Func<..., TResult> — с результатом, Predicate<T> — это Func<T, bool>. Главная ловушка: передача Func<T,bool> в IQueryable вместо Expression<Func<T,bool>> тянет всю таблицу в память.
Action, Func и Predicate — встроенные делегаты .NET
Action, Func и Predicate — это предопределённые обобщённые типы делегатов из пространства имён System, избавляющие от необходимости объявлять собственный тип делегата для каждой сигнатуры.
Action<T1, T2, ...>— делегат без возвращаемого значения (void), до 16 параметров.Func<T1, T2, ..., TResult>— делегат, возвращающийTResult; последний тип-параметр — тип результата.Predicate<T>— сокращение дляFunc<T, bool>, исторически появился в .NET 2.0 вместе сList<T>.FindAll.
// Action — выполняет побочный эффект, ничего не возвращает
Action<string> log = msg => Console.WriteLine($"[LOG] {msg}");
log("Запрос получен");
// Func — вычисляет результат
Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 4); // 7
// Func с захватом переменной из замыкания
int multiplier = 5;
Func<int, int> times = n => n * multiplier;
Console.WriteLine(times(6)); // 30
// Predicate — проверяет условие
Predicate<string> isValid = s => !string.IsNullOrWhiteSpace(s);
var names = new List<string> { "Alice", "", "Bob", null! };
List<string> valid = names.FindAll(isValid); // ["Alice", "Bob"]
// Multicast-делегат: Action поддерживает цепочку
Action<int> pipeline = n => Console.Write($"Step1({n}) ");
pipeline += n => Console.Write($"Step2({n}) ");
pipeline(42); // Step1(42) Step2(42)
Когда использовать каждый тип
- Используйте
Actionдля коллбэков, обработчиков событий и «fire-and-forget» операций без результата. - Используйте
Funcдля преобразований, фабрик, LINQ-предикатов и мест, где нужен возвращаемый результат. - Используйте
Predicate<T>только там, где API явно его требует (List<T>.FindAll,Array.FindAll); в остальных случаях предпочтитеFunc<T, bool>. - Для выражений в LINQ (деревья выражений) используйте
Expression<Func<T, bool>>, а не голыйFunc— EF Core не сможет транслировать лямбду в SQL без обёрткиExpression.
Подводные камни
- Замыкание на переменную цикла. В
for/foreachлямбда захватывает переменную, а не её значение. В C# 5+foreachсоздаёт новую переменную на каждой итерации, но вforпо-прежнему нужна локальная копия:var i2 = i; Action f = () => Console.WriteLine(i2); - Multicast и исключения. Если к
Actionподписаны несколько обработчиков и один бросает исключение, остальные не вызываются — в отличие отevent, где это обычно ожидаемо. - Func vs Expression. Передача
Func<T, bool>вIQueryable.WhereвызываетAsEnumerable()неявно и тянет все строки в память; нуженExpression<Func<T, bool>>. - Предпочитайте именованные методы для горячих путей. Лямбды в
Func/Actionсоздают объект делегата при каждом присваивании — кешируйте их в поле, если вызываются на критическом пути. - Nullable в аргументах.
Predicate<string>не защищает отnullпо умолчанию — добавляйтеArgumentNullException.ThrowIfNullили null-guard явно. - Смешивание async.
Actionне возвращаетTask, поэтомуasync void Action-лямбды не позволяют корректно обрабатывать ошибки. Если нужен async-коллбэк, используйтеFunc<Task>.
Common mistakes
- Путать готовые делегаты Action, Func и Predicate с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы C#: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет готовые делегаты Action, Func и Predicate на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.