1. Introdução
Em muitos projetos C# modernos quase não se usam métodos "comuns" para tratar eventos. Isso aconteceu porque expressões lambda permitem declarar rapidamente e de forma concisa um handler direto no ponto de assinatura (operador +=), quando o tratamento é simples e não é reutilizado em outras partes do código. É parecido com colar um bilhete pequeno direto na máquina de café com a instrução "pressione este botão" em vez de escrever um manual detalhado e guardar numa pasta separada. Se a tarefa for local e única, a lambda é ideal!
Onde isso é usado na prática
- No ASP.NET (por exemplo, ao lidar com eventos do ciclo de vida da página),
- No WPF/WinForms para UI (por exemplo, ao clicar em um botão),
- Em programação de servidor (por exemplo, lógica dentro do pipeline),
- Em testes, quando o handler não precisa de um nome separado.
Sintaxe: como isso fica no código
Vamos primeiro ver o tratamento de evento "tradicional":
// Declaração do evento
public event EventHandler? MyEvent;
// Assinatura do evento com um método comum
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("Evento ocorreu!");
}
public void Subscribe()
{
MyEvent += Handler;
}
Agora — a versão com expressão lambda (função anônima):
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("Evento ocorreu (lambda)!");
}
Repare: não criamos um método separado, definimos o handler diretamente na assinatura do evento. A assinatura da lambda bate automaticamente com o tipo do evento (EventHandler).
Comparação de abordagens
| Método comum | Lambda | |
|---|---|---|
| Volume de código | Maior (método + assinatura) | Menor, tudo no lugar |
| Reutilização | Pode ser reutilizado | Normalmente não |
| Localidade do código | Distribuído | Tudo junto |
| Clareza/legibilidade | Bom para lógica complexa | Ótimo para casos simples |
2. Aplicação com tratamento de eventos e lambdas
Estrutura básica
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"Item do menu {index} selecionado.");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
Assinatura com expressão lambda
class Program
{
static void Main()
{
var menu = new Menu();
// Assinatura no evento via lambda
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("Obrigado pela sua escolha! O lambda-handler foi disparado.");
};
menu.SelectItem(1);
}
}
Saída esperada:
Item do menu 1 selecionado.
Obrigado pela sua escolha! O lambda-handler foi disparado.
Captura de variáveis do contexto externo
Uma das vantagens da expressão lambda é que ela pode "lembrar" valores do escopo externo (closures). Por exemplo, contar quantas vezes um item do menu foi selecionado: a variável counter será capturada pelo closure.
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"Item selecionado {counter} vez(es)!");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
Saída esperada:
Item do menu 1 selecionado.
Item selecionado 1 vez(es)!
Item do menu 2 selecionado.
Item selecionado 2 vez(es)!
Isso é a mágica dos closures em ação: a variável counter continua viva dentro da lambda!
Exemplo com parâmetros do evento
Se seu evento usa EventHandler<T>, onde T é uma classe própria com informação adicional, a expressão lambda simplesmente "se ajusta" à assinatura necessária.
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"Item do menu {index}: {description} selecionado.");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// Uso
static void Main()
{
var menu = new Menu();
// Lambda com desempacotamento dos argumentos do evento
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"Item escolhido #{args.ItemIndex}: {args.Description.ToUpper()}");
};
menu.SelectItem(3, "Sobre o programa");
}
Saída esperada:
Item do menu 3: Sobre o programa selecionado.
Item escolhido #3: SOBRE O PROGRAMA
3. Handlers locais e lambda
Lambdas são ideais quando:
- A lógica do handler é curta e clara,
- O handler é usado só em um lugar,
- É preciso "capturar" variáveis do contexto local.
Se o tratamento é complexo, precisa ser reutilizado ou pode ser chamado fora do local de declaração, é melhor usar um método nomeado separado.
Exemplo: com lógica dentro e fora
Lambda (ideal):
button.Click += (s, e) => MessageBox.Show("Botão pressionado!");
Métodos (quando a lógica é mais complexa ou precisa ser reutilizada):
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("Dados salvos!");
}
}
4. Por baixo do capô: o que acontece com lambda-handlers
A lambda também é um delegate, assim como um handler comum. O compilador gera "on the fly" um método anônimo, e se houver variáveis capturadas, também gera uma classe escondida para armazená-las.
É importante lembrar (e isso é um erro comum): se você cria uma lambda dentro de um loop e a assina a um evento, todas as iterações podem capturar a mesma variável do loop.
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Clique no botão #{i}");
};
}
Para todos os handlers, o número do botão pode acabar sendo 5! Para evitar isso, crie uma cópia da variável dentro do loop:
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // Cópia local
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Clique no botão #{buttonIndex}");
};
}
Agora tudo funciona como esperado.
O poder dos lambda-handlers: vida real
Essas expressões lambda economizam tempo e aceleram o desenvolvimento, especialmente quando a lógica é simples. Elas deixam o código "próximo da tarefa", sem espalhá-lo por arquivos e classes. Em projetos reais você vai encontrar lambdas por toda parte: desde tratamento de eventos na UI até assinatura de mensagens em buses de eventos assíncronos.
5. Erros típicos
Erro nº1: esquecer de desinscrever (-=) em objetos de longa duração.
Se o objeto assinante não se desinscrever do evento do publisher, a referência no delegate mantém o assinante na memória — o garbage collector não vai coletá-lo, mesmo que não haja outras referências ao objeto. Isso causa "vazamentos" de memória e dependências pendentes, especialmente quando o publisher vive por muito tempo (eventos estáticos, singletons, serviços).
Como evitar: sempre desinscreva ao liberar/destruir (por exemplo, Dispose, OnDisable, OnDestroy). Considere weak events / WeakEventManager, usar um padrão de "event manager" ou IObservable/Rx se o cenário for complexo.
Erro nº2: capturar variáveis do loop sem cópia local.
Uma armadilha típica é escrever a assinatura dentro de um loop e o closure capturar a mesma variável do loop, então todos os handlers veem o valor final dela, não o valor que existia quando o handler foi criado. Isso leva a resultados inesperados (todos os handlers "imprimem" o mesmo número, etc.).
Como evitar: dentro do loop crie uma cópia local do valor e capture-a:
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
Ou passe o valor necessário para um método wrapper. Isso é mais robusto e deixa a intenção clara.
Erro nº3: usar lambdas enormes no ponto de assinatura.
Se você coloca lógica de negócio grande dentro de uma função anônima na assinatura, o código fica difícil de ler, testar e depurar. Além disso — com lambdas anônimas é mais difícil desinscrever corretamente, porque você precisa do mesmo instance do delegate. Como resultado, a lógica se espalha e perde estrutura.
Como evitar: extraia lógica complexa para métodos nomeados ou services; se precisar, armazene o delegate em uma variável/field para poder desinscrever; deixe na lambda apenas um pequeno wrapper/roteamento. Isso melhora legibilidade e manutenibilidade do código.
GO TO FULL VERSION