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.

Você vai aprender
  • 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 pela List<T>.
  • Guardar métodos prontos (Console.WriteLine) num Action ou Func.
  • Escrever seus próprios métodos que recebem um Func ou um Action.
  • Um método que fabrica lambdas: devolve um Func.
  • Os erros reais: CS0029, CS1662, CS0201, CS0305 e CS1593.

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 prontoFormatoTradução
Func<…, TResult>recebe de 0 a 16 valores e devolve umfunction, "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
TipoRecebeExemplo de chamada
Actionnada (sem <>)abrir()
Action<string>um stringavisar("Acabou o troco")
Action<string, int>um string e um int, nessa ordemrepor("Caderno", 20)
Como escolher entre 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;: o Console.WriteLine tem várias versões (para int, decimal, string…). O tipo Action<string> diz qual delas escolher: a que recebe um string.
  • texto[..tamanho] pega os primeiros tamanho caracteres (veja Índices e Intervalos).
  • nomes.ForEach(escrever) passa o Action para o ForEach, que o chama para cada nome. Também daria para escrever direto nomes.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, que p é um Produto.

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çãoUse
Um parâmetro ou variável que recebe uma lambda, no dia a diaFunc 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 inUm delegate próprio: Func e Action não aceitam esses modificadores.
EventosO delegate pronto EventHandler, assunto da próxima aula, Eventos.

Quando dá errado

CS0029: 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.

CS0029 + CS1662: a ordem dos tipos do 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}".

CS0201: um 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;.

CS0305: 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 <>.

CS1593: chamar com argumentos que o 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

TipoRecebeDevolveExemplo
Func<TResult>nadaTResultFunc<string> f = () => "oi";
Func<T, TResult>um TTResultFunc<decimal, decimal> f = p => p * 0.7m;
Func<T1, T2, TResult>T1 e T2TResultFunc<decimal, int, decimal> f = (p, q) => p * q;
ActionnadanadaAction a = () => Console.WriteLine("oi");
Action<T>um TnadaAction<string> a = Console.WriteLine;
Action<T1, T2>T1 e T2nadaAction<string, int> a = (n, q) => …;
Predicate<T>um TboolPredicate<Produto> b = p => p.Preco < 5m;
Comparison<T>dois Tintlista.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:

mostrar = p => Console.WriteLine(p.Nome);

3. Recebe dois, devolve um

Complete o tipo de total, que recebe um preço (decimal) e uma quantidade (int) e devolve um decimal:

total = (p, q) => p * q;

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étodos void: todos os tipos são parâmetros, e Action sozinho não recebe nada.
  • Predicate<T> recebe um T e devolve bool; é o que Find, FindAll, Exists e RemoveAll pedem.
  • Com eles, você quase nunca precisa declarar um delegate: escreva Func<Produto, bool> condicao no parâmetro e passe uma lambda.
  • Um método pode devolver um Func e "fabricar" lambdas que lembram dos parâmetros.
  • Predicate<T> e Func<T, bool> são tipos diferentes (CS0029); Func sem tipos dá CS0305.

Próxima aula: Eventos, os delegates que avisam "aconteceu alguma coisa".