1. Introdução
Eventos em C# não são só variáveis onde você guarda um delegate. São uma lista protegida de handlers, e só o dono do evento pode dispará-lo; os outros só podem adicionar (+=) ou remover (-=) suas reações.
Por exemplo, assim você pode se inscrever num evento:
worker.WorkCompleted += Worker_WorkCompleted;
À primeira vista parece uma soma comum, mas na prática funciona diferente. Por baixo do capô o evento guarda uma invocation list — um conjunto de delegates que precisam ser chamados quando o evento dispara. Quando você escreve +=, um novo handler é adicionado a essa lista.
Vamos entender em mais detalhe o que acontece internamente, como essa cadeia é formada e que nuances podem surgir ao assinar.
O que é uma cadeia de delegates?
Lembra que delegates em C# são "multicast": você pode associar vários métodos e todos serão executados na ordem se o delegate for invocado. Eventos usam esse mecanismo: o valor deles é, na prática, um delegate com uma lista de handlers.
Em termos de código:
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
Quando alguém se inscreve:
worker.WorkCompleted += MyHandler;
O C# por baixo faz mais ou menos assim:
- Pega o delegate atual (a lista de handlers).
- Chama Delegate.Combine para unir os handlers.
- Grava a cadeia atualizada de volta na variável do evento.
Esquematicamente:
| Operação | Lista interna de handlers do evento |
|---|---|
| Antes | null ou [Handler1] |
| Depois de += Handler2 | [Handler1, Handler2] |
| Depois de mais um += H3 | [Handler1, Handler2, Handler3] |
Curiosidade: o mecanismo de multicast delegates não é "mágica", é uma implementação concreta: por baixo o delegate contém um array de métodos a serem chamados.
2. Como funciona a assinatura: explicação simples
Vamos ver com um exemplo concreto. Suponha que temos um publisher (Worker) e subscribers (Logger, Notifier):
public class Worker
{
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
public void DoWork()
{
// ... trabalho ...
OnWorkCompleted("Zadanie zaversheno!");
}
protected virtual void OnWorkCompleted(string message)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
}
}
public class Logger
{
public void LogWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine("Log: " + e.Message);
}
}
public class Notifier
{
public void ShowNotification(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine("Uvedomlenie: " + e.Message);
}
}
No Main:
var worker = new Worker();
var logger = new Logger();
var notifier = new Notifier();
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += notifier.ShowNotification;
// Inicia o trabalho
worker.DoWork();
Quando OnWorkCompleted é chamado, o evento primeiro invoca logger.LogWorkCompleted, depois notifier.ShowNotification (na ordem em que você se inscreveu).
3. Nuances úteis
Visualização: como o evento guarda os handlers
+---------------------+
| Worker |
|---------------------|
| WorkCompleted Event | ---> [ LogWorkCompleted, ShowNotification ]
+---------------------+
Ao assinar, cada novo handler "se prende" à lista existente. Depois do disparo, o delegate chama sequencialmente todos os métodos inscritos.
Assinatura múltipla com o mesmo método
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += logger.LogWorkCompleted; // Duas vezes!
Nesse caso o handler será chamado tantas vezes quantas estiver inscrito — aqui serão duas chamadas seguidas.
Assinar com uma lambda
worker.WorkCompleted += (sender, e) => Console.WriteLine("Anonymnyi handler: " + e.Message);
Se você inscrever essa lambda várias vezes — vai ser chamada várias vezes também. Mas atenção: cada lambda cria um objeto delegate distinto, então não é trivial remover depois (veja a aula 260 em diante para detalhes).
Como a assinatura funciona internamente: análise de baixo nível
Evento é uma propriedade especial com dois acessores (add/remove) que são chamados quando se usa += e -=. Simplificando, o compilador gera algo como:
// Mais ou menos assim (simplificado)
public event EventHandler<WorkCompletedEventArgs> WorkCompleted
{
add { /* código de adição do handler */ }
remove { /* código de remoção do handler */ }
}
Por padrão a implementação usa Delegate.Combine e Delegate.Remove.
Isso protege o evento: externamente você não pode invocar o evento diretamente (nada de worker.WorkCompleted(...);), só pode assinar ou desassinar.
Dinâmica da assinatura: desenhando o esquema
+----------------------+
| |
v v
+--------------------+ +----------------------+
| LogWorkCompleted | | ShowNotification |
+--------------------+ +----------------------+
^ ^
\______________________/
^
|
WorkCompleted Event
A tal "Invocation List" — a cadeia de chamadas.
Por que isso importa: significado prático
Entender a mecânica de assinatura é chave pra gerenciar dependências entre objetos. Você pode montar sistemas onde componentes se inscrevem e desinscrevem dinamicamente sem criar acoplamento forte. Isso é padrão em frameworks UI, engines de jogos, servidores e até em arquiteturas modernas de microservices (lá a ideia fica na camada de filas, mas o princípio é parecido).
Em entrevistas costumam perguntar como o modelo de eventos do C# funciona, por que eventos deixam o sistema flexível e como gerenciar lifecycle de assinaturas corretamente.
4. O que se pode (e não se pode) fazer de fora da classe
Pode
- Assinar o evento (+=)
- Desassinar (-=)
Não pode
- Invocar o evento diretamente
- Atribuir um delegate ao evento diretamente (worker.WorkCompleted = ... — erro!)
Essas restrições são implementadas pela palavra-chave event. Se você declarasse o delegate como um campo comum:
public EventHandler<WorkCompletedEventArgs> WorkCompleted; // ne event!
— qualquer um poderia fazer de tudo com ele, até zerar os handlers, o que provocaria bagunça e bugs potenciais. Por isso quase sempre use somente event!
É possível assinar o mesmo método em vários eventos?
Sim! Isso se chama "multisubscribe". Por exemplo:
worker.WorkCompleted += logger.LogWorkCompleted;
anotherWorker.WorkCompleted += logger.LogWorkCompleted;
Se você assinou o mesmo método em eventos de objetos diferentes, o handler será executado para ambos. Dentro do handler dá pra distinguir quem disparou pelo parâmetro sender.
Caso real: assinaturas dinâmicas
Imagina um app onde o usuário roda várias tarefas em paralelo. Pra cada tarefa se cria um Worker e assina o mesmo handler — por exemplo, logger.LogWorkCompleted.
var allWorkers = new List<Worker>();
for (int i = 0; i < 10; i++)
{
var w = new Worker();
w.WorkCompleted += logger.LogWorkCompleted;
allWorkers.Add(w);
}
Assim, quando qualquer tarefa terminar, o logger recebe a notificação e registra o que rolou e quando.
Prática: como ver os subscribers de um evento
No código comum não dá pra saber diretamente quantos handlers estão inscritos (o evento encapsula o delegate). Mas dentro da classe publisher você pode acessar o delegate e usar GetInvocationList():
// Só dentro da classe publisher
var handlers = WorkCompleted?.GetInvocationList();
if (handlers != null)
Console.WriteLine($"Podpischikov: {handlers.Length}");
Isso é útil se você precisa fazer lógica custom de broadcast ou debug (embora seja melhor usar só para fins educativos!).
5. Erros típicos e particularidades
Handler não foi adicionado? Não será chamado!
Se você não se inscreveu, o handler nunca vai rodar. Antes de disparar o evento sempre verifique null (senão rola NullReferenceException).
Assinar várias vezes
Se você assinou múltiplas vezes o mesmo método no mesmo evento — ele será chamado múltiplas vezes. Às vezes isso pega a galera desprevenida: um += duplicado transforma uma notificação em várias idênticas.
Métodos estáticos/instância
Você pode assinar tanto métodos static quanto de instância. O importante é a assinatura bater com a delegate signature.
public static void StaticHandler(object? sender, WorkCompletedEventArgs e) { /* ... */ }
worker.WorkCompleted += StaticHandler;
Lambdas e assinatura dentro de loop
Se você, por exemplo, em um loop assina lambdas, cuide do que a lambda captura. Dá pra acabar capturando um valor diferente do que você espera.
GO TO FULL VERSION