Menu

Func и Action в C#: обобщённые делегаты, Predicate и примеры

Как работают Func и Action в C#: Func<T, TResult> для функций, которые возвращают значение, Action<T> для тех, что не возвращают, Predicate и Comparison, передача и возврат функций, словарь команд, асинхронные лямбды и когда объявлять собственный делегат.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Func и Action это обобщённые типы делегатов, которые .NET объявляет за вас, поэтому функции можно хранить и передавать, не объявляя сначала тип делегата. Func возвращает значение; Action ничего не возвращает.

Вывод:

4
46.00
False
Hello!
Mia scored 87

Правило, которое нужно запомнить: в Func последний аргумент типа это возвращаемый тип. Func<int, int, bool> принимает два int и возвращает bool. Func<int> ничего не принимает и возвращает int. Все аргументы типа у Action это параметры.

Семейство

ТипПараметрыВозвращает
Actionнетvoid
Action<T>Tvoid
Action<T1, T2> ... до 16T1, T2, ...void
Func<TResult>нетTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... до 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (отрицательное, ноль, положительное)
Converter<TIn, TOut>TInTOut

Predicate, Comparison и Converter старше и встречаются в методах List<T> и Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ везде использует Func, поэтому list.Where(...) и list.FindAll(...) принимают лямбды одной формы, но разных типов:

Вывод:

17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y

Передача функций в методы

Параметр Func или Action позволяет вызывающему коду подставить изменяемую часть алгоритма. Две распространённые формы это «выполни и измерь» и «пробуй, пока не получится»:

Вывод:

attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2

Retry обобщённый, поэтому его Func<int, T> может возвращать что угодно; компилятор вывел T как string из лямбды. Лямбда, переданная в Twice, меняет локальную переменную clicks, и это работает, потому что лямбда захватывает саму переменную, а не копию её значения.

Группы методов: передача метода по имени

Чтобы заполнить Func или Action, лямбда не нужна. Имя существующего метода с подходящей сигнатурой («группа методов») преобразуется напрямую:

Вывод:

parsed: 43
sat is weekend: True
total quantity: 20

Select(int.Parse) это то же самое, что Select(s => int.Parse(s)), только без одного уровня вызова. Компилятор выбирает перегрузку, подходящую под целевой тип: у int.Parse несколько перегрузок, и только Parse(string) соответствует Func<string, int>. Когда компилятор не может выбрать одну (несколько перегрузок подходят одинаково хорошо или аргументы типа обобщённого метода нельзя вывести из группы методов), вызов не компилируется, и лямбда, делающая вызов явным, это исправляет.

Группа методов, использованная как Func, всё равно выполняется позже, когда вызывается делегат, в точности как лямбда. Разница только в записи.

Возврат функций

Метод может построить и вернуть функцию. Возвращённый Func несёт значения, из которых он был построен:

Вывод:

140
180
162

Twice(staff) строит новую функцию, которая применяет скидку для сотрудников дважды: 200, затем 180, затем 162.

Словарь функций

Хранение функций в словаре превращает длинный switch в таблицу поиска. Это обычная форма для обработчиков команд, калькуляторов и действий меню, а новые элементы можно добавлять во время выполнения:

Вывод:

12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE

Добавление оператора это ещё один элемент словаря без изменений в цикле. StringComparer.OrdinalIgnoreCase, переданный во второй словарь, делает имена команд нечувствительными к регистру.

Асинхронные функции: Func<Task>, а не Action

В асинхронном коде тип делегата решает, может ли вызывающий код дождаться работы:

Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save();                       // the caller can await it and see its exceptions

Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);

Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad();                              // async void: fire and forget, exceptions escape

Асинхронная лямбда, присвоенная Action, становится методом async void: её ничто не может ожидать, а исключение внутри неё никогда не доходит до вызывающего кода. Оно повторно выбрасывается в пуле потоков (или в потоке интерфейса в настольном приложении), где обычно обрушивает процесс. Когда вы пишете метод, принимающий асинхронную работу, принимайте Func<Task> (или Func<Task<T>>).

Когда объявлять собственный делегат

Func и Action покрывают большинство потребностей. Объявляйте именованный тип делегата, когда:

  • В сигнатуре есть параметры ref, out или in. Func не может их выразить, поэтому функции в стиле TryParse нужен delegate bool TryParser<T>(string text, out T value);.
  • Имя документирует роль в открытом API. delegate bool Validator(string input) в сигнатуре метода говорит больше, чем Func<string, bool>, а имена его параметров видны в IntelliSense.
  • Нужен параметр params, которого у обобщённых делегатов тоже быть не может.

Ещё одно место, где встречаются собственные типы делегатов, это события, хотя большинство из них покрывает встроенный EventHandler<TEventArgs>. Объявление и объединение типов делегатов описаны на странице о делегатах.

Часто задаваемые вопросы

Чем Func отличается от Action в C#?

Func возвращает значение, а Action нет. В Func<int, string, bool> последний аргумент типа (bool) это возвращаемый тип, а остальные это параметры. Action<int, string> принимает int и string и возвращает void. Оба существуют в вариантах с числом параметров от 0 до 16.

Как передать функцию как параметр в C#?

Объявите параметр как Func или Action нужной формы и вызовите его внутри метода: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Вызывающий код передаёт лямбду (Apply(80m, p => p * 0.9m)) или имя метода (Apply(80m, HalfPrice)).

Что такое Predicate<T> в C#?

Тип делегата, который принимает T и возвращает bool, той же формы, что и Func<T, bool>. Он старше Func и используется в List<T>.Find, FindAll, RemoveAll, Exists и Array.Find. Эти два типа не преобразуются друг в друга, но лямбда преобразуется в любой из них.

Как использовать Func с асинхронным кодом?

Используйте Func<Task> для асинхронной функции без результата и Func<T, Task<TResult>> для функции с результатом, чтобы вызывающий код мог её ожидать через await. Не присваивайте асинхронную лямбду Action: это делает её async void, которую нельзя ожидать и исключения которой вызывающий код не может перехватить.

Могут ли у Func быть параметры out или ref?

Нет. Обобщённые параметры Func и Action это обычные параметры по значению, поэтому написать Func<string, out int, bool> нельзя. Для сигнатуры вроде TryParse объявите собственный делегат: delegate bool TryParser<T>(string text, out T value);.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ