Func e Action são tipos delegate genéricos que o .NET declara para você, então dá para guardar e passar funções sem declarar um tipo delegate antes. Func retorna um valor; Action não retorna nada.
Saída:
4
46.00
False
Hello!
Mia scored 87
A regra para lembrar: em um Func, o último argumento de tipo é o tipo de retorno. Func<int, int, bool> recebe dois int e retorna um bool. Func<int> não recebe nada e retorna um int. Os argumentos de tipo de um Action são todos parâmetros.
A família
| Tipo | Parâmetros | Retorna |
|---|---|---|
Action | nenhum | void |
Action<T> | T | void |
Action<T1, T2> ... até 16 | T1, T2, ... | void |
Func<TResult> | nenhum | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... até 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (negativo, zero, positivo) |
Converter<TIn, TOut> | TIn | TOut |
Predicate, Comparison e Converter são mais antigos e aparecem nos métodos de List<T> e Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). O LINQ usa Func em todo lugar, e é por isso que list.Where(...) e list.FindAll(...) recebem lambdas da mesma forma, mas de tipos diferentes:
Saída:
17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y
Passando funções para métodos
Um parâmetro Func ou Action permite que quem chama forneça a parte de um algoritmo que varia. Duas formas comuns são "execute isto e meça" e "tente isto até funcionar":
Saída:
attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2
Retry é genérico, então o Func<int, T> dele pode retornar qualquer coisa; o compilador inferiu T como string a partir da lambda. A lambda passada para Twice muda a variável local clicks, o que funciona porque uma lambda captura a própria variável, não uma cópia do valor.
Grupos de métodos: passando um método pelo nome
Você não precisa de uma lambda para preencher um Func ou Action. O nome de um método existente com assinatura compatível (um "grupo de métodos") é convertido diretamente:
Saída:
parsed: 43
sat is weekend: True
total quantity: 20
Select(int.Parse) é o mesmo que Select(s => int.Parse(s)), com uma camada de chamada a menos. O compilador escolhe a sobrecarga que se encaixa no tipo de destino: int.Parse tem várias sobrecargas, e só Parse(string) corresponde a Func<string, int>. Quando o compilador não consegue decidir por uma (várias sobrecargas se encaixam igualmente bem, ou os argumentos de tipo de um método genérico não podem ser inferidos a partir de um grupo de métodos), a chamada não compila, e uma lambda que deixa a chamada explícita resolve.
Um grupo de métodos usado como Func ainda executa depois, sempre que o delegate é invocado, exatamente como uma lambda. A diferença está só na forma de escrever.
Retornando funções
Um método pode montar e retornar uma função. O Func retornado carrega os valores com que foi montado:
Saída:
140
180
162
Twice(staff) monta uma função nova que aplica o desconto de funcionário duas vezes: 200, depois 180, depois 162.
Um dicionário de funções
Guardar funções em um dicionário transforma um switch longo em uma tabela de consulta. É a forma usual de handlers de comandos, calculadoras e ações de menu, e novas entradas podem ser adicionadas em tempo de execução:
Saída:
12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE
Adicionar um operador é uma entrada a mais no dicionário, sem nenhuma mudança no laço. O StringComparer.OrdinalIgnoreCase passado para o segundo dicionário faz os nomes de comandos ignorarem maiúsculas e minúsculas.
Funções async: Func<Task>, não Action
Com código async, o tipo delegate decide se quem chama pode esperar o trabalho terminar:
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
Uma lambda async atribuída a um Action vira um método async void: nada pode aguardá-la, e uma exceção dentro dela nunca chega a quem chamou. Ela é relançada no thread pool (ou na thread de interface em um app desktop), onde normalmente derruba o processo. Quando você escreve um método que aceita trabalho async, receba um Func<Task> (ou Func<Task<T>>).
Quando declarar seu próprio delegate
Func e Action cobrem a maioria das necessidades. Declare um tipo delegate com nome quando:
- A assinatura tem parâmetros
ref,outouin.Funcnão consegue expressá-los, então uma função no estiloTryParseprecisa dedelegate bool TryParser<T>(string text, out T value);. - O nome documenta um papel em uma API pública.
delegate bool Validator(string input)na assinatura de um método diz mais queFunc<string, bool>, e os nomes dos parâmetros aparecem no IntelliSense. - Você precisa de um parâmetro
params, que delegates genéricos também não podem ter.
Os eventos são o outro lugar onde tipos delegate próprios ainda aparecem, embora o EventHandler<TEventArgs> embutido cubra a maior parte deles. A página de delegates cobre como declarar e combinar tipos delegate.
Perguntas frequentes
Qual a diferença entre Func e Action em C#?
Func retorna um valor e Action não. Em Func<int, string, bool>, o último argumento de tipo (bool) é o tipo de retorno e os outros são parâmetros. Action<int, string> recebe um int e uma string e retorna void. Os dois existem em versões de 0 a 16 parâmetros.
Como passar uma função como parâmetro em C#?
Declare o parâmetro como um Func ou Action com a forma certa e chame-o dentro do método: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Quem chama passa uma lambda (Apply(80m, p => p * 0.9m)) ou o nome de um método (Apply(80m, HalfPrice)).
O que é Predicate<T> em C#?
Um tipo delegate que recebe um T e retorna bool, a mesma forma de Func<T, bool>. Ele é anterior ao Func e é usado por List<T>.Find, FindAll, RemoveAll, Exists e Array.Find. Os dois tipos não se convertem um no outro, mas uma lambda se converte em qualquer um deles.
Como usar Func com código async?
Use Func<Task> para uma função async sem resultado e Func<T, Task<TResult>> para uma com resultado, para que quem chama possa usar await nela. Evite atribuir uma lambda async a um Action: isso a transforma em async void, que não pode ser aguardada e cujas exceções não podem ser capturadas por quem chama.
Func pode ter parâmetros out ou ref?
Não. Os parâmetros genéricos de Func e Action são parâmetros comuns por valor, então não há como escrever Func<string, out int, bool>. Para uma assinatura como a de TryParse, declare um delegate próprio: delegate bool TryParser<T>(string text, out T value);.