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.

Você vai aprender
  • 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, CS0201 e CS8820.

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 que p é um decimal. O nome é livre; p, preco ou x dã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
FormaExemploRegra
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âmetrop => p / 2Os parênteses são opcionais.
Um parâmetro, com parênteses(p) => p + 8mO 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 * qtdParê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.

Expressão ou bloco?

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
LinhaO 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 / 100mO 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.

Novidade do C# 14: 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 promocao foi criada uma vez só, mas a segunda chamada já usou 0.50m: ela lê o desconto na hora de rodar.
  • A lambda registrar muda a variável vendas lá de fora, e a mudança fica: no fim, vendas vale 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ável i para o laço inteiro. As três lambdas capturam a mesma. Quando elas finalmente rodam, o laço já acabou e i vale 3 (foi o valor que fez o i < 3 falhar).
  • No foreach, cada volta tem uma variável n nova 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étodoA lambda recebe……e devolveO método faz
ForEach(p => …)cada elementonadaExecuta a lambda para cada um, na ordem.
Find(p => …)cada elementotrue/falseDevolve o primeiro que deu true, ou null se nenhum deu (por isso o Produto? e o caro?.Nome).
Exists(p => …)cada elementotrue/falseDiz se algum deu true.
Sort((a, b) => …)dois elementosnegativo, zero ou positivoOrdena a própria lista. a.Preco.CompareTo(b.Preco) devolve negativo se a é mais barato: ordem crescente. Veja IComparable e IComparer.
ConvertAll(p => …)cada elementoo novo valorCria uma lista nova com os resultados (aqui, só os nomes).
RemoveAll(p => …)cada elementotrue/falseRemove 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çãoUse
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 lugaresMétodo com nome, passado sem parênteses: produtos.Sort(CompararPorPreco)
A regra precisa de um nome para o código ficar claroMé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 nadaLambda static C# 9: static p => p * 0.7m. Se ela usar uma variável de fora, dá CS8820.
A forma antiga: 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

CS8917: lambda em 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;).

CS1643: o bloco nem sempre devolve um valor

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).

CS0201 + CS1643: esqueceu o 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.

CS1593: número de parâmetros diferente do delegate

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.

CS0029 + CS1662: a lambda devolve o tipo errado

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.

CS8820: lambda 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

FormaExemploEquivale a um método…
() => expr() => 42sem parâmetros que devolve 42
x => exprp => p * 0.7mde um parâmetro
(a, b) => expr(preco, qtd) => preco * qtdde dois parâmetros
(tipo a) => expr(int x) => x * 2com o tipo escrito (permite var)
(a, b) => { …; return r; }(p, q) => { var t = p * q; return t; }com várias instruções
tipo (…) => exprdecimal (int n) => n / 100mcom o retorno escrito C# 10
(a = valor) => …(string nome = "cliente") => …com parâmetro opcional C# 12
(_, _) => expr(_, _) => 0mque ignora os parâmetros C# 9
static x => exprstatic p => p * 2que não captura nada C# 9

Exercícios

1. O dobro

Complete a lambda que recebe p e devolve o dobro:

RegraDePreco dobro =

2. Sem parâmetros

Complete a lambda de uma Saudacao (sem parâmetros, void) que mostra Oi:

Saudacao oi =

3. Achar na lista

Complete para achar o primeiro produto que custa menos de 5m:

Produto? barato = produtos.Find();

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 var e vira um Func ou Action; 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, RemoveAll recebem lambdas.
  • Erros comuns: caminho sem return (CS1643), parâmetros a mais (CS1593), retorno de outro tipo (CS1662), static com 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.