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> | T | void |
Action<T1, T2> ... до 16 | T1, T2, ... | void |
Func<TResult> | нет | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... до 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (отрицательное, ноль, положительное) |
Converter<TIn, TOut> | TIn | TOut |
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);.