C# Func, Action e Predicate
Nas aulas de Delegates e Lambdas, cada lambda precisou de um tipo delegate declarado por nós: delegate decimal RegraDePreco(decimal preco);. Se cada programa tivesse de inventar um delegate para "recebe um decimal, devolve um decimal", haveria milhares de delegates iguais com nomes diferentes. Por isso o .NET já traz delegates prontos, que servem para quase tudo: Func, Action e Predicate. Eles são os tipos que você vai ver em toda a biblioteca, inclusive no LINQ.
Func<…>: para métodos que devolvem um valor, e como ler os tipos entre<e>.Action<…>: para métodos que não devolvem nada (void).Predicate<T>: para perguntas de sim ou não, usado pelaList<T>.- Guardar métodos prontos (
Console.WriteLine) numActionouFunc. - Escrever seus próprios métodos que recebem um
Funcou umAction. - Um método que fabrica lambdas: devolve um
Func. - Os erros reais:
CS0029,CS1662,CS0201,CS0305eCS1593.
Todos os programas rodaram com o .NET 10 (C# 14) na máquina da Ana, na pasta ~/cs, com dotnet run arquivo.cs. Func, Action e Predicate moram no namespace System, que já vem importado: não precisa de nenhum using.
A ideia: as tomadas padronizadas
Imagine se cada aparelho da lojinha da Ana viesse com um plugue de formato próprio: um para a máquina de cartão, outro para a balança, outro para o ventilador. Ela teria de instalar uma tomada nova para cada aparelho. Por isso existem as tomadas padronizadas: dois ou três formatos servem para quase tudo.
Os delegates que você declara (RegraDePreco, Aviso) são tomadas sob medida. Func, Action e Predicate são as tomadas padrão do .NET:
| Delegate pronto | Formato | Tradução |
|---|---|---|
Func<…, TResult> | recebe de 0 a 16 valores e devolve um | function, "função" |
Action<…> | recebe de 0 a 16 valores e não devolve nada | "ação" |
Predicate<T> | recebe um valor e devolve bool | "predicado": uma afirmação que é verdadeira ou falsa |
Os tipos entre < e > são os argumentos de tipo de um tipo genérico: eles dizem o que entra e o que sai, e assim um só Func serve para qualquer combinação.
1. Func: recebe e devolve
Func<decimal, decimal> blackFriday = p => p * 0.7m; Func<decimal, int, decimal> total = (preco, qtd) => preco * qtd; Func<string> saudacao = () => "Bem-vindo à lojinha!"; Console.WriteLine(blackFriday(50m)); Console.WriteLine(total(2.50m, 3)); Console.WriteLine(saudacao());
$ dotnet run func.cs 35,0 7,50 Bem-vindo à lojinha!
A regra para ler um Func: o último tipo é sempre o retorno; os outros, na ordem, são os parâmetros.
Func<decimal, decimal> « recebe decimal → devolve decimal
Func<decimal, int, decimal> « recebe decimal e int → devolve decimal
Func<string> « não recebe nada → devolve string
└─ entradas ─┘ └ saída ┘
Compare com o delegate da aula anterior. As duas linhas abaixo guardam a mesma lambda; a diferença é que a primeira precisa da declaração delegate decimal RegraDePreco(decimal preco); no fim do arquivo, e a segunda não precisa de nada:
RegraDePreco regra1 = p => p * 0.7m; // tipo próprio, declarado por você Func<decimal, decimal> regra2 = p => p * 0.7m; // tipo pronto do .NET
Na saída, 35,0 aparece com uma casa porque o decimal guarda as casas da conta (50 × 0,7: uma casa). Para dinheiro, mostre com :C, como nas outras aulas.
2. Action: recebe e não devolve
Para métodos void, que fazem algo (escrever, salvar, avisar) em vez de calcular, use Action. Todos os tipos entre < e > são parâmetros, porque não há retorno:
Action abrir = () => Console.WriteLine("Loja aberta!");
Action<string> avisar = msg => Console.WriteLine($"[aviso] {msg}");
Action<string, int> repor = (produto, qtd) =>
Console.WriteLine($"Repor {qtd} unidades de {produto}");
abrir();
avisar("Acabou o troco");
repor("Caderno", 20);
$ dotnet run action.cs Loja aberta! [aviso] Acabou o troco Repor 20 unidades de Caderno
| Tipo | Recebe | Exemplo de chamada |
|---|---|---|
Action | nada (sem <>) | abrir() |
Action<string> | um string | avisar("Acabou o troco") |
Action<string, int> | um string e um int, nessa ordem | repor("Caderno", 20) |
Func e Action
Pergunte: "o método devolve alguma coisa?". Se devolve, é Func, e o tipo devolvido vai por último. Se é void, é Action. Um Func sempre tem pelo menos um tipo entre < e > (o retorno); um Action pode não ter nenhum.
Missão: siga os comentários. Crie um Func<decimal, decimal> chamado comFrete, que soma 8m ao preço, e um Action<decimal> chamado mostrar, que escreve Preço: seguido do valor com :C. No fim, chame mostrar(comFrete(20m));. A saída deve ser Preço: R$ 28,00.
// 1. crie aqui o Func<decimal, decimal> comFrete (soma 8m ao preço)
// 2. crie aqui o Action<decimal> mostrar (escreve "Preço: " e o valor com :C)
// 3. troque a linha abaixo por: mostrar(comFrete(20m));
Console.WriteLine("Preço: ?");
Func<decimal, decimal> comFrete = p => p + 8m;
Action<decimal> mostrar = p => Console.WriteLine($"Preço: {p:C}");
mostrar(comFrete(20m));
3. Predicate: sim ou não
Um Predicate<T> recebe um valor e responde true ou false: "este produto é barato?". É o tipo que os métodos Find, FindAll, Exists e RemoveAll da List<T> recebem:
List<Produto> produtos =
[
new("Caderno", 18.90m),
new("Lápis", 2.50m),
new("Mochila", 129.90m),
new("Borracha", 1.75m),
];
Predicate<Produto> barato = p => p.Preco < 5m;
Console.WriteLine(barato(produtos[0]));
Console.WriteLine(produtos.Exists(barato));
Console.WriteLine(produtos.Find(barato)?.Nome);
List<Produto> baratos = produtos.FindAll(barato);
Console.WriteLine(string.Join(", ", baratos.ConvertAll(p => p.Nome)));
record Produto(string Nome, decimal Preco);
$ dotnet run predicate.cs False « o caderno (18,90) não é barato True « existe algum barato Lápis « o primeiro barato Lápis, Borracha « todos os baratos
A vantagem de guardar a pergunta numa variável com nome (barato) é usá-la várias vezes, e o código se lê quase como português: produtos.Exists(barato), "existe algum produto barato?".
Predicate<T> e Func<T, bool> fazem a mesma coisa
Os dois recebem um T e devolvem bool. O Predicate é mais antigo (.NET 2.0) e ficou nos métodos da List<T>; o LINQ, que veio depois, usa Func<T, bool>. Uma lambda escrita direto serve para os dois, mas uma variável de um tipo não pode ir para o outro (veja o erro CS0029 em Quando dá errado).
Existe ainda o Comparison<T>, que recebe dois valores e devolve um int (negativo, zero ou positivo). É o tipo que o Sort((a, b) => …) da aula de Lambdas recebe.
4. Guardar métodos que já existem
Como qualquer delegate, Func e Action guardam também métodos com nome, inclusive os do próprio .NET. Escreva o nome sem parênteses:
List<string> nomes = ["caderno", "lápis", "mochila"];
Action<string> escrever = Console.WriteLine;
Func<string, string> gritar = Maiusculas;
Func<string, int, string> cortar = Cortar;
escrever(gritar("promoção"));
escrever(cortar("borracha", 4));
nomes.ForEach(escrever);
static string Maiusculas(string texto) => texto.ToUpper();
static string Cortar(string texto, int tamanho) => texto[..tamanho];
$ dotnet run grupos.cs PROMOÇÃO borr caderno lápis mochila
Action<string> escrever = Console.WriteLine;: oConsole.WriteLinetem várias versões (paraint,decimal,string…). O tipoAction<string>diz qual delas escolher: a que recebe umstring.texto[..tamanho]pega os primeirostamanhocaracteres (veja Índices e Intervalos).nomes.ForEach(escrever)passa oActionpara oForEach, que o chama para cada nome. Também daria para escrever diretonomes.ForEach(Console.WriteLine).
5. Seus métodos recebendo Func e Action
A parte mais útil: escrever métodos que recebem um comportamento. O método Filtrar abaixo não sabe qual filtro aplicar; quem chama é que diz, com uma lambda. E o ParaCada não sabe o que fazer com cada produto:
List<Produto> produtos =
[
new("Caderno", 18.90m),
new("Lápis", 2.50m),
new("Mochila", 129.90m),
new("Borracha", 1.75m),
];
ParaCada(Filtrar(produtos, p => p.Preco < 20m), p => Console.WriteLine($"{p.Nome}: {p.Preco:C}"));
Console.WriteLine("---");
ParaCada(Filtrar(produtos, p => p.Nome.StartsWith('M')), p => Console.WriteLine(p.Nome));
List<Produto> Filtrar(List<Produto> lista, Func<Produto, bool> condicao)
{
List<Produto> resultado = [];
foreach (Produto p in lista)
{
if (condicao(p))
{
resultado.Add(p);
}
}
return resultado;
}
void ParaCada(List<Produto> lista, Action<Produto> acao)
{
foreach (Produto p in lista)
{
acao(p);
}
}
record Produto(string Nome, decimal Preco);
$ dotnet run filtrar.cs Caderno: R$ 18,90 Lápis: R$ 2,50 Borracha: R$ 1,75 --- Mochila
- Func<Produto, bool> condicao
- O parâmetro recebe "algo que, dado um
Produto, responde sim ou não". - if (condicao(p))
- Chama a lambda recebida para cada produto. Se ela disser
true, o produto entra no resultado. - Action<Produto> acao
- Recebe "algo que faz alguma coisa com um
Produto" e não devolve nada. - Filtrar(produtos, p => p.Preco < 20m)
- Na chamada, a lambda vira o
condicao. O compilador sabe, pelo tipo do parâmetro, quepé umProduto.
Guarde este Filtrar na memória: ele é, quase linha por linha, o Where do LINQ. Você acabou de escrever um pedaço do LINQ à mão.
6. Um método que fabrica lambdas
Um Func também pode ser o retorno de um método. O CriarDesconto abaixo recebe uma porcentagem e devolve uma regra de preço pronta:
Func<decimal, decimal> dezPorCento = CriarDesconto(10);
Func<decimal, decimal> metade = CriarDesconto(50);
Console.WriteLine(dezPorCento(80m));
Console.WriteLine(metade(80m));
Func<decimal, decimal> CriarDesconto(int porcento)
{
return p => p - p * porcento / 100;
}
$ dotnet run fabrica.cs 72 40
A lambda p => p - p * porcento / 100 captura o parâmetro porcento (veja a captura em Lambdas). Cada chamada de CriarDesconto cria uma lambda com o seu próprio porcento: a dezPorCento lembra do 10, a metade lembra do 50, mesmo depois de o método ter terminado.
Delegate próprio ou pronto: qual usar
| Situação | Use |
|---|---|
| Um parâmetro ou variável que recebe uma lambda, no dia a dia | Func ou Action. Quem lê já conhece, e não há nada para declarar. |
Uma pergunta de sim ou não para a List<T> | Predicate<T> (é o que Find, FindAll, Exists e RemoveAll pedem). Para o LINQ, Func<T, bool>. |
O nome do tipo explica muito (RegraDePreco diz mais que Func<decimal, decimal>) | Um delegate próprio. |
Parâmetros ref, out ou in | Um delegate próprio: Func e Action não aceitam esses modificadores. |
| Eventos | O delegate pronto EventHandler, assunto da próxima aula, Eventos. |
Quando dá errado
Predicate não vira Func
Este código tem um erro de propósito.
Predicate<decimal> caro = p => p > 100m; Func<decimal, bool> mesmaCoisa = caro; Console.WriteLine(mesmaCoisa(150m));
$ dotnet run predicado.cs /home/ana/cs/predicado.cs(2,34): error CS0029: Não é possível converter implicitamente tipo "System.Predicate<decimal>" em "System.Func<decimal, bool>" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: mesmo com o mesmo formato (recebe decimal, devolve bool), são tipos diferentes, como duas classes diferentes com as mesmas propriedades. O C# não converte um no outro sozinho.
Como consertar: embrulhe numa lambda, Func<decimal, bool> mesmaCoisa = p => caro(p);, ou crie o outro tipo a partir dele, new Func<decimal, bool>(caro). Melhor ainda: escolha um tipo só desde o começo.
Func
Este código tem um erro de propósito.
Func<decimal, string> etiqueta = p => p * 2;
$ dotnet run ordem.cs /home/ana/cs/ordem.cs(1,39): error CS0029: Não é possível converter implicitamente tipo "decimal" em "string" /home/ana/cs/ordem.cs(1,39): error CS1662: Não é possível converter expressão lambda para o tipo delegate pretendido porque alguns dos tipos de retorno no bloco não são implicitamente conversíveis para o tipo de retorno delegate Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: em Func<decimal, string>, o último tipo (string) é o retorno. A lambda devolve p * 2, um decimal.
Como consertar: se a ideia era "recebe decimal, devolve decimal", escreva Func<decimal, decimal>. Se era uma etiqueta de texto, devolva texto: p => $"{p * 2:C}".
Action não devolve nada
Este código tem um erro de propósito.
Action<string> tamanho = s => s.Length;
$ dotnet run acao.cs /home/ana/cs/acao.cs(1,31): error CS0201: Somente as expressões de atribuição, chamada, incremento, diminuição, espera e novo objeto podem ser utilizadas como uma instrução Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: num Action, o corpo da lambda tem de ser uma instrução que faz algo. s.Length só lê um valor, que seria jogado fora.
Como consertar: se você quer o valor, use Func<string, int> tamanho = s => s.Length;.
Func sem os tipos
Este código tem um erro de propósito.
Func calcular = () => 10m;
$ dotnet run semtipos.cs /home/ana/cs/semtipos.cs(1,1): error CS0305: Usar o genérico tipo "Func<TResult>" requer 1 argumentos de tipo Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: "usar o tipo genérico Func<TResult> requer 1 argumento de tipo". Todo Func precisa de pelo menos o tipo do retorno (TResult, "tipo do resultado").
Como consertar: Func<decimal> calcular = () => 10m;. Só o Action existe sem <>.
Func não tem
Este código tem um erro de propósito.
Func<decimal> preco = () => 10m; Console.WriteLine(preco(5m));
$ dotnet run chamada.cs /home/ana/cs/chamada.cs(2,19): error CS1593: Delegado "Func<decimal>" não obtém 1 argumentos Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: Func<decimal> tem um tipo só, e ele é o retorno: o delegate não recebe nada. Um erro comum é achar que Func<decimal> "recebe um decimal".
Como consertar: chame sem argumentos (preco()) ou, se ele deve receber um preço, declare Func<decimal, decimal>.
Referência
| Tipo | Recebe | Devolve | Exemplo |
|---|---|---|---|
Func<TResult> | nada | TResult | Func<string> f = () => "oi"; |
Func<T, TResult> | um T | TResult | Func<decimal, decimal> f = p => p * 0.7m; |
Func<T1, T2, TResult> | T1 e T2 | TResult | Func<decimal, int, decimal> f = (p, q) => p * q; |
Action | nada | nada | Action a = () => Console.WriteLine("oi"); |
Action<T> | um T | nada | Action<string> a = Console.WriteLine; |
Action<T1, T2> | T1 e T2 | nada | Action<string, int> a = (n, q) => …; |
Predicate<T> | um T | bool | Predicate<Produto> b = p => p.Preco < 5m; |
Comparison<T> | dois T | int | lista.Sort((a, b) => a.Preco.CompareTo(b.Preco)); |
Func e Action vão até 16 parâmetros. Se você precisar de mais de 3 ou 4, junte os dados num record.
Exercícios
1. Ler o tipo
Qual é o tipo devolvido por um Func<string, int, bool>?
2. Escolher o tipo
Complete o tipo da variável que guarda um método que recebe um Produto e não devolve nada:
3. Recebe dois, devolve um
Complete o tipo de total, que recebe um preço (decimal) e uma quantidade (int) e devolve um decimal:
4. Qual destes tipos guarda () => Console.WriteLine("Loja aberta!")?
5. Func<decimal> f = () => 10m;. Como chamar f?
6. Uma variável Predicate<Produto> pode ser passada direto para um parâmetro Func<Produto, bool>?
Missão final: o método Contar recebe um Func<Produto, bool> condicao, mas ignora a condição e conta todos os produtos. Conserte para contar só os produtos em que condicao(p) é verdadeira. A saída deve ser Até R$ 20: 3 e Começam com M: 1.
List<Produto> produtos =
[
new("Caderno", 18.90m),
new("Lápis", 2.50m),
new("Mochila", 129.90m),
new("Borracha", 1.75m),
];
Console.WriteLine($"Até R$ 20: {Contar(produtos, p => p.Preco <= 20m)}");
Console.WriteLine($"Começam com M: {Contar(produtos, p => p.Nome.StartsWith('M'))}");
int Contar(List<Produto> lista, Func<Produto, bool> condicao)
{
int quantos = 0;
foreach (Produto p in lista)
{
quantos++;
}
return quantos;
}
record Produto(string Nome, decimal Preco);
List<Produto> produtos =
[
new("Caderno", 18.90m),
new("Lápis", 2.50m),
new("Mochila", 129.90m),
new("Borracha", 1.75m),
];
Console.WriteLine($"Até R$ 20: {Contar(produtos, p => p.Preco <= 20m)}");
Console.WriteLine($"Começam com M: {Contar(produtos, p => p.Nome.StartsWith('M'))}");
int Contar(List<Produto> lista, Func<Produto, bool> condicao)
{
int quantos = 0;
foreach (Produto p in lista)
{
if (condicao(p))
{
quantos++;
}
}
return quantos;
}
record Produto(string Nome, decimal Preco);
Na saída do código inicial, as duas linhas mostram 4: a condição nunca é chamada. No LINQ, esse método já existe pronto: produtos.Count(p => p.Preco <= 20m).
Resumo
Func<…, TResult>guarda métodos que devolvem um valor: o último tipo é o retorno.Action<…>guarda métodosvoid: todos os tipos são parâmetros, eActionsozinho não recebe nada.Predicate<T>recebe umTe devolvebool; é o queFind,FindAll,ExistseRemoveAllpedem.- Com eles, você quase nunca precisa declarar um
delegate: escrevaFunc<Produto, bool> condicaono parâmetro e passe uma lambda. - Um método pode devolver um
Funce "fabricar" lambdas que lembram dos parâmetros. Predicate<T>eFunc<T, bool>são tipos diferentes (CS0029);Funcsem tipos dáCS0305.
Próxima aula: Eventos, os delegates que avisam "aconteceu alguma coisa".