C# Lambdas
Na aula de Delegates, cada regra de preço da lojinha da Ana virou um método com nome: PrecoNormal, BlackFriday. Para uma regra de uma linha, usada num lugar só, isso é muita cerimônia. Uma lambda é um método sem nome, escrito direto onde vai ser usado: p => p * 0.7m quer dizer "dado p, devolva p * 0.7m". As lambdas estão em todo o C# moderno, e o LINQ é feito praticamente só delas.
- O que é uma lambda e como ela substitui um método com nome.
- Todas as formas: sem parâmetro, um, dois, com tipo, com bloco e
return. - Guardar uma lambda em
var(o "tipo natural"), parâmetros com valor padrão e o descarte_. - Como uma lambda captura variáveis de fora, e a armadilha do
for. - Lambdas com listas:
ForEach,Find,Exists,Sort,RemoveAll. - Os erros reais:
CS8917,CS1643,CS1593,CS1662,CS0201eCS8820.
Todos os programas rodaram com o .NET 10 (C# 14) na máquina da Ana, na pasta ~/cs, com dotnet run arquivo.cs.
A ideia: o bilhete no lugar do manual
A Ana vai sair mais cedo e deixa o caixa com o Bruno. Ela poderia escrever um manual, com capa e título ("Procedimento Black Friday"), e dizer "siga o manual". Mas a regra é curta, então ela deixa um bilhete: "preço vezes 0,7". O bilhete não tem título: tem só a instrução.
O método com nome é o manual. A lambda é o bilhete: só os parâmetros, uma seta e o que fazer. Ela vale em qualquer lugar onde se espera um delegate.
1. Método com nome × lambda
RegraDePreco comMetodo = BlackFriday;
RegraDePreco comLambda = p => p * 0.7m;
Console.WriteLine($"Método: {comMetodo(50m):C}");
Console.WriteLine($"Lambda: {comLambda(50m):C}");
Console.WriteLine($"Direto: {Aplicar(50m, p => p - 5m):C}");
decimal Aplicar(decimal preco, RegraDePreco regra) => regra(preco);
static decimal BlackFriday(decimal preco) => preco * 0.7m;
delegate decimal RegraDePreco(decimal preco);
$ dotnet run metodo-lambda.cs Método: R$ 35,00 Lambda: R$ 35,00 Direto: R$ 45,00
As duas primeiras linhas fazem a mesma coisa. Compare as peças:
static decimal BlackFriday(decimal preco) => preco * 0.7m; « manual: nome, tipos e corpo
p => p * 0.7m « bilhete: só o que importa
- p
- O parâmetro. Não precisa do tipo: o compilador olha para a variável (
RegraDePreco) e deduz quepé umdecimal. O nome é livre;p,precoouxdão no mesmo. - =>
- A seta (sinal de igual + sinal de maior). Lê-se "vai para" ou "resulta em". É o mesmo
=>dos métodos de uma linha. - p * 0.7m
- O corpo. Se for uma expressão só, o valor dela é devolvido automaticamente, sem
return. O tipo de retorno (decimal) também vem do delegate.
A terceira linha mostra o maior ganho: a lambda p => p - 5m foi escrita direto no argumento de Aplicar, sem existir em nenhum outro lugar do programa. O nome "lambda" vem da letra grega λ, usada pelos matemáticos para escrever funções sem nome.
Missão: o frete da lojinha custa R$ 5,00 por produto. Troque a lambda p => p por uma que some 5m ao preço. A última linha deve ser R$ 7,90 -> R$ 12,90.
decimal[] precos = [12.50m, 30m, 7.90m];
MostrarPrecos(precos, p => p);
void MostrarPrecos(decimal[] lista, RegraDePreco regra)
{
foreach (decimal p in lista)
{
Console.WriteLine($"{p:C} -> {regra(p):C}");
}
}
delegate decimal RegraDePreco(decimal preco);
decimal[] precos = [12.50m, 30m, 7.90m];
MostrarPrecos(precos, p => p + 5m);
void MostrarPrecos(decimal[] lista, RegraDePreco regra)
{
foreach (decimal p in lista)
{
Console.WriteLine($"{p:C} -> {regra(p):C}");
}
}
delegate decimal RegraDePreco(decimal preco);
2. Todas as formas
Uma lambda se adapta ao delegate onde ela é guardada: sem parâmetros, com um, com dois, curta ou com várias linhas.
Saudacao ola = () => Console.WriteLine("Bem-vindo à lojinha!");
RegraDePreco metade = p => p / 2;
RegraDePreco frete = (p) => p + 8m;
RegraDePreco arredondar = (decimal p) => Math.Round(p);
Conta total = (preco, qtd) => preco * qtd;
Conta comTaxa = (preco, qtd) =>
{
decimal bruto = preco * qtd;
decimal taxa = bruto * 0.05m;
return bruto + taxa;
};
ola();
Console.WriteLine(metade(9m));
Console.WriteLine(frete(20m));
Console.WriteLine(arredondar(4.6m));
Console.WriteLine(total(2.5m, 4));
Console.WriteLine(comTaxa(10m, 3));
delegate void Saudacao();
delegate decimal RegraDePreco(decimal preco);
delegate decimal Conta(decimal preco, int quantidade);
$ dotnet run formas.cs Bem-vindo à lojinha! 4,5 28 5 10,0 31,50
| Forma | Exemplo | Regra |
|---|---|---|
| Sem parâmetros | () => Console.WriteLine("…") | Os parênteses vazios são obrigatórios. O delegate Saudacao é void, então a lambda não devolve nada. |
| Um parâmetro | p => p / 2 | Os parênteses são opcionais. |
| Um parâmetro, com parênteses | (p) => p + 8m | O mesmo que o anterior. |
| Com o tipo escrito | (decimal p) => Math.Round(p) | Com tipo, os parênteses são obrigatórios. Se um parâmetro tem tipo, todos precisam ter. |
| Dois ou mais | (preco, qtd) => preco * qtd | Parênteses obrigatórios, separados por vírgula. preco vira decimal e qtd vira int, na ordem do delegate Conta. |
| Corpo com bloco | (preco, qtd) => { …; return bruto + taxa; } | Com chaves, é um bloco normal: cada instrução termina com ; e é preciso return (se o delegate devolve algo). Repare no ; depois da }: ele fecha a linha Conta comTaxa = …. |
Os números da saída: 9m / 2 dá 4,5; Math.Round(4.6m) dá 5; 2.5m * 4 dá 10,0 (o decimal guarda as casas da conta: uma casa no 2.5, uma casa no resultado); e 10 × 3 + 5% dá 31,50.
Se a lambda cabe numa expressão, escreva sem chaves: p => p * 0.7m. Se precisa de várias linhas, use o bloco com return. Se o bloco passou de 3 ou 4 linhas, ele merece virar um método com nome: o nome explica o que o código faz, e o método pode ser testado sozinho.
3. Lambda em var: o tipo natural C# 10
Até agora, cada lambda foi guardada numa variável de um tipo delegate declarado por nós. Desde o C# 10, se a lambda tem os tipos dos parâmetros escritos, ela pode ir para uma variável var, e o compilador escolhe um delegate pronto do .NET:
var dobro = (int x) => x * 2;
var saudar = (string nome = "cliente") => $"Olá, {nome}!";
var porcento = decimal (int n) => n / 100m;
Console.WriteLine(dobro(21));
Console.WriteLine(dobro.GetType());
Console.WriteLine(saudar());
Console.WriteLine(saudar("Bruno"));
Console.WriteLine(porcento(15));
$ dotnet run natural.cs 42 System.Func`2[System.Int32,System.Int32] « Func<int, int>: recebe int, devolve int Olá, cliente! Olá, Bruno! 0,15
| Linha | O que mostra |
|---|---|
var dobro = (int x) => x * 2; | Com o tipo do parâmetro, o compilador sabe tudo e escolhe o tipo Func<int, int>. O Func é o delegate pronto do .NET para "recebe algo, devolve algo" (aula Func, Action e Predicate). O `2 que o GetType() mostra quer dizer "com 2 tipos entre < e >". |
(string nome = "cliente") => … | Parâmetro com valor padrão C# 12, como num método (veja Parâmetros Opcionais). Sem argumento, vale "cliente". |
decimal (int n) => n / 100m | O tipo de retorno escrito antes dos parênteses C# 10. Raro, mas útil quando o compilador não consegue adivinhar o retorno. |
Sem o tipo do parâmetro, não dá: var dobro = x => x * 2; dá o erro CS8917, porque x poderia ser um int, um decimal, um double… (veja Quando dá errado).
Parâmetros que você não usa: _ C# 9
Às vezes o delegate exige parâmetros que a lambda não usa. Em vez de inventar nomes, use o descarte _ (sublinhado): (_, _) => 0m é uma lambda de dois parâmetros que ignora os dois e devolve zero. Isso aparece bastante em eventos.
out e ref sem escrever o tipo C# 14
Se o delegate tem um parâmetro out (veja ref, out e in), até o C# 13 a lambda precisava escrever todos os tipos: (string texto, out decimal valor) => …. No C# 14, basta o modificador: com delegate bool Leitor(string texto, out decimal valor);, a lambda Leitor ler = (texto, out valor) => decimal.TryParse(texto, out valor); compila no .NET 10, e ler("12,90", out decimal preco) devolve true com preco valendo 12,90.
4. A lambda enxerga as variáveis de fora (captura)
Uma lambda pode usar as variáveis do lugar onde foi escrita. Isso se chama capturar a variável. O detalhe importante: ela captura a variável, não uma cópia do valor. Se a variável muda depois, a lambda vê o valor novo:
decimal desconto = 0.10m;
RegraDePreco promocao = p => p - p * desconto;
Console.WriteLine(promocao(100m));
desconto = 0.50m;
Console.WriteLine(promocao(100m));
int vendas = 0;
Aviso registrar = produto =>
{
vendas++;
Console.WriteLine($"Venda {vendas}: {produto}");
};
registrar("Caderno");
registrar("Lápis");
Console.WriteLine($"Total de vendas: {vendas}");
delegate decimal RegraDePreco(decimal preco);
delegate void Aviso(string produto);
$ dotnet run captura.cs 90,00 « desconto = 10% 50,00 « a MESMA lambda, agora com desconto = 50% Venda 1: Caderno Venda 2: Lápis Total de vendas: 2
- A lambda
promocaofoi criada uma vez só, mas a segunda chamada já usou0.50m: ela lê odescontona hora de rodar. - A lambda
registrarmuda a variávelvendaslá de fora, e a mudança fica: no fim,vendasvale 2. Isso é permitido em C# (em Java, por exemplo, não é).
Uma lambda com as variáveis que ela capturou forma um fechamento (closure, em inglês): a variável continua viva enquanto a lambda existir, mesmo que o método onde ela nasceu já tenha terminado.
A armadilha do for
Como a lambda captura a variável e não o valor, um for pode surpreender. O programa abaixo guarda três lambdas numa lista e só depois roda todas:
List<Tarefa> comFor = [];
for (int i = 0; i < 3; i++)
{
comFor.Add(() => Console.Write($"{i} "));
}
foreach (Tarefa t in comFor) t();
Console.WriteLine("« for");
List<Tarefa> comForeach = [];
foreach (int n in new[] { 0, 1, 2 })
{
comForeach.Add(() => Console.Write($"{n} "));
}
foreach (Tarefa t in comForeach) t();
Console.WriteLine("« foreach");
List<Tarefa> consertado = [];
for (int i = 0; i < 3; i++)
{
int copia = i;
consertado.Add(() => Console.Write($"{copia} "));
}
foreach (Tarefa t in consertado) t();
Console.WriteLine("« for com cópia");
delegate void Tarefa();
$ dotnet run laco.cs 3 3 3 « for 0 1 2 « foreach 0 1 2 « for com cópia
- No
for, existe uma variávelipara o laço inteiro. As três lambdas capturam a mesma. Quando elas finalmente rodam, o laço já acabou eivale 3 (foi o valor que fez oi < 3falhar). - No
foreach, cada volta tem uma variávelnnova C# 5. Cada lambda captura a sua. - O conserto do
for: crie uma variável dentro do bloco (int copia = i;). Ela nasce de novo a cada volta, e cada lambda fica com a sua cópia.
5. Lambdas com listas
É com coleções que as lambdas aparecem mais. A List<T> tem vários métodos que recebem uma lambda dizendo o que fazer com cada elemento:
List<Produto> produtos =
[
new("Caderno", 18.90m),
new("Lápis", 2.50m),
new("Mochila", 129.90m),
new("Borracha", 1.75m),
];
produtos.ForEach(p => Console.WriteLine($"{p.Nome}: {p.Preco:C}"));
Produto? caro = produtos.Find(p => p.Preco > 100m);
Console.WriteLine($"Find: {caro?.Nome}");
bool temBarato = produtos.Exists(p => p.Preco < 2m);
Console.WriteLine($"Exists: {temBarato}");
produtos.Sort((a, b) => a.Preco.CompareTo(b.Preco));
Console.WriteLine($"Sort: {string.Join(", ", produtos.ConvertAll(p => p.Nome))}");
int removidos = produtos.RemoveAll(p => p.Preco < 3m);
Console.WriteLine($"RemoveAll: {removidos} removidos, sobrou {produtos.Count}");
record Produto(string Nome, decimal Preco);
$ dotnet run listas.cs Caderno: R$ 18,90 Lápis: R$ 2,50 Mochila: R$ 129,90 Borracha: R$ 1,75 Find: Mochila Exists: True Sort: Borracha, Lápis, Caderno, Mochila RemoveAll: 2 removidos, sobrou 2
O record Produto(string Nome, decimal Preco); no fim cria um tipo com as propriedades Nome e Preco (veja Records). Cada new("Caderno", 18.90m) cria um produto.
| Método | A lambda recebe… | …e devolve | O método faz |
|---|---|---|---|
ForEach(p => …) | cada elemento | nada | Executa a lambda para cada um, na ordem. |
Find(p => …) | cada elemento | true/false | Devolve o primeiro que deu true, ou null se nenhum deu (por isso o Produto? e o caro?.Nome). |
Exists(p => …) | cada elemento | true/false | Diz se algum deu true. |
Sort((a, b) => …) | dois elementos | negativo, zero ou positivo | Ordena a própria lista. a.Preco.CompareTo(b.Preco) devolve negativo se a é mais barato: ordem crescente. Veja IComparable e IComparer. |
ConvertAll(p => …) | cada elemento | o novo valor | Cria uma lista nova com os resultados (aqui, só os nomes). |
RemoveAll(p => …) | cada elemento | true/false | Remove os que deram true e devolve quantos removeu. |
O LINQ leva essa ideia muito mais longe, com Where, Select, OrderBy e outros, todos recebendo lambdas.
Método com nome ou lambda: qual usar
| Situação | Use |
|---|---|
| A regra é curta (uma expressão) e usada num lugar só | Lambda: produtos.Find(p => p.Preco > 100m) |
| A regra tem várias linhas, ou é usada em vários lugares | Método com nome, passado sem parênteses: produtos.Sort(CompararPorPreco) |
| A regra precisa de um nome para o código ficar claro | Método com nome, ou uma variável com bom nome: RegraDePreco blackFriday = p => p * 0.7m; |
| Você quer garantir que a lambda não captura nada | Lambda static C# 9: static p => p * 0.7m. Se ela usar uma variável de fora, dá CS8820. |
delegate (…) { … }
Antes das lambdas (C# 3, de 2007), escrevia-se um método anônimo: RegraDePreco r = delegate (decimal p) { return p * 0.7m; };. Ainda compila, e você vai encontrar em código antigo, mas hoje se usa a lambda.
Quando dá errado
var sem o tipo do parâmetro
Este código tem um erro de propósito.
var dobro = x => x * 2; Console.WriteLine(dobro(21));
$ dotnet run natural-erro.cs /home/ana/cs/natural-erro.cs(1,13): error CS8917: O tipo de representante não pôde ser inferido. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: "o tipo do delegate (o 'representante', na tradução) não pôde ser deduzido". Com var, não há nenhum tipo do lado esquerdo para dizer o que é x.
Como consertar: escreva o tipo do parâmetro (var dobro = (int x) => x * 2;) ou o tipo da variável (Func<int, int> dobro = x => x * 2;).
Este código tem um erro de propósito.
RegraDePreco promo = p =>
{
if (p > 100m)
{
return p * 0.8m;
}
};
delegate decimal RegraDePreco(decimal preco);
$ dotnet run caminhos.cs /home/ana/cs/caminhos.cs(1,24): error CS1643: Nem todos os caminhos de código retornam um valor em expressão lambda do tipo "RegraDePreco" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: se p for 100 ou menos, a lambda chega ao fim sem return, e o delegate prometeu devolver um decimal.
Como consertar: devolva algo em todos os caminhos: um return p; depois do if. Ou, numa expressão só: p => p > 100m ? p * 0.8m : p (veja Operador Ternário).
return no bloco
Este código tem um erro de propósito.
RegraDePreco promo = p => { p * 0.8m; };
delegate decimal RegraDePreco(decimal preco);
$ dotnet run semreturn.cs /home/ana/cs/semreturn.cs(1,29): error CS0201: Somente as expressões de atribuição, chamada, incremento, diminuição, espera e novo objeto podem ser utilizadas como uma instrução /home/ana/cs/semreturn.cs(1,24): error CS1643: Nem todos os caminhos de código retornam um valor em expressão lambda do tipo "RegraDePreco" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: com chaves, a lambda é um bloco comum, e uma conta solta (p * 0.8m;) não é uma instrução que faça alguma coisa (CS0201). Sem return, também falta o valor devolvido (CS1643).
Como consertar: p => { return p * 0.8m; }, ou melhor ainda, sem chaves: p => p * 0.8m.
Este código tem um erro de propósito.
RegraDePreco promo = (p, q) => p * 0.8m; delegate decimal RegraDePreco(decimal preco);
$ dotnet run parametros.cs /home/ana/cs/parametros.cs(1,29): error CS1593: Delegado "RegraDePreco" não obtém 2 argumentos Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: a lambda tem 2 parâmetros, mas o delegate RegraDePreco recebe só 1.
Como consertar: a lambda precisa ter o mesmo número de parâmetros do delegate: p => p * 0.8m.
Este código tem um erro de propósito.
RegraDePreco gratis = p => "grátis"; delegate decimal RegraDePreco(decimal preco);
$ dotnet run tipo.cs /home/ana/cs/tipo.cs(1,28): error CS0029: Não é possível converter implicitamente tipo "string" em "decimal" /home/ana/cs/tipo.cs(1,28): 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: o delegate promete devolver decimal, e a lambda devolve um texto. O primeiro erro diz qual conversão falhou; o segundo, que por isso a lambda não serve para esse delegate.
Como consertar: devolva o tipo certo (p => 0m para "grátis"). Se você queria um texto, o delegate é que tem de ser outro.
static usando variável de fora
Este código tem um erro de propósito.
decimal desconto = 0.1m; RegraDePreco promo = static p => p - p * desconto; Console.WriteLine(promo(10m)); delegate decimal RegraDePreco(decimal preco);
$ dotnet run estatica.cs /home/ana/cs/estatica.cs(2,42): error CS8820: Uma função anônima estática não pode conter uma referência a 'desconto'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: o static promete que a lambda não captura nada, e ela captura desconto.
Como consertar: tire o static, ou passe o desconto por parâmetro, ou use uma constante (const decimal Desconto = 0.1m;), que pode ser usada numa lambda estática.
Referência
| Forma | Exemplo | Equivale a um método… |
|---|---|---|
() => expr | () => 42 | sem parâmetros que devolve 42 |
x => expr | p => p * 0.7m | de um parâmetro |
(a, b) => expr | (preco, qtd) => preco * qtd | de dois parâmetros |
(tipo a) => expr | (int x) => x * 2 | com o tipo escrito (permite var) |
(a, b) => { …; return r; } | (p, q) => { var t = p * q; return t; } | com várias instruções |
tipo (…) => expr | decimal (int n) => n / 100m | com o retorno escrito C# 10 |
(a = valor) => … | (string nome = "cliente") => … | com parâmetro opcional C# 12 |
(_, _) => expr | (_, _) => 0m | que ignora os parâmetros C# 9 |
static x => expr | static p => p * 2 | que não captura nada C# 9 |
Exercícios
1. O dobro
Complete a lambda que recebe p e devolve o dobro:
2. Sem parâmetros
Complete a lambda de uma Saudacao (sem parâmetros, void) que mostra Oi:
3. Achar na lista
Complete para achar o primeiro produto que custa menos de 5m:
4. Qual destas linhas não compila?
5. decimal taxa = 1m; RegraDePreco r = p => p + taxa; taxa = 2m; Console.WriteLine(r(10m)); mostra…
6. Três lambdas criadas num for (int i = 0; i < 3; i++) mostram i. Rodando depois do laço, elas mostram…
Missão final: usando só lambdas (sem for nem foreach), tire da lista os produtos de mais de R$ 100,00 com RemoveAll, ordene do mais caro para o mais barato com Sort e mostre com o ForEach que já existe. A saída deve ser exatamente Caderno: R$ 18,90, Lápis: R$ 2,50 e Borracha: R$ 1,75, nessa ordem. Dica: para inverter a ordem, troque a e b de lugar no CompareTo.
List<Produto> produtos =
[
new("Caderno", 18.90m),
new("Lápis", 2.50m),
new("Mochila", 129.90m),
new("Borracha", 1.75m),
];
produtos.ForEach(p => Console.WriteLine($"{p.Nome}: {p.Preco:C}"));
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),
];
produtos.RemoveAll(p => p.Preco > 100m);
produtos.Sort((a, b) => b.Preco.CompareTo(a.Preco));
produtos.ForEach(p => Console.WriteLine($"{p.Nome}: {p.Preco:C}"));
record Produto(string Nome, decimal Preco);
Resumo
- Uma lambda é um método sem nome:
parâmetros => corpo. Ela vale onde se espera um delegate. - Os tipos dos parâmetros e o retorno vêm do delegate. Formas:
() => …,p => …,(a, b) => …,(decimal p) => …e o bloco{ …; return …; }. - Com os tipos escritos, a lambda vai para
vare vira umFuncouAction; sem eles,CS8917. - A lambda captura a variável, não o valor: vê as mudanças e pode mudá-la. No
for, crie uma cópia dentro do bloco. List<T>:ForEach,Find,Exists,Sort,ConvertAll,RemoveAllrecebem lambdas.- Erros comuns: caminho sem
return(CS1643), parâmetros a mais (CS1593), retorno de outro tipo (CS1662),staticcom captura (CS8820).
Próxima aula: Func, Action e Predicate, os delegates prontos do .NET, para você não precisar declarar um tipo a cada lambda.