1. Introdução
De certa forma, assinar um evento em C# é como se inscrever na newsletter de memes de um amigo: as novidades chegam até você enquanto você não disser "chega" e não cancelar a inscrição. Em programação isso é especialmente importante, porque uma inscrição esquecida não é só "mais um meme", é um vazamento de memória!
Imagine que você tem um formulário na aplicação (por exemplo, uma janela de configurações). Ele se inscreve em um evento da janela principal para reagir a mudanças. O usuário fecha o formulário achando que ele foi destruído, mas o handler ainda está inscrito! O formulário continua vivo na memória porque o objeto principal mantém referência a ele através do evento.
Conclusão: Se o objeto-subscriber se inscreveu no evento do publisher e "esqueceu" de cancelar, o garbage collector não vai removê-lo da memória enquanto o publisher estiver vivo.
Relembrando o operador += e mostrando -=
- += — inscrição: adiciona o handler à lista de chamadas do evento.
- -= — cancelamento: remove o handler da lista de chamadas.
Fica mais ou menos assim:
worker.WorkCompleted += handler; // inscrição
worker.WorkCompleted -= handler; // cancelamento
Se um handler foi adicionado duas vezes, você precisa removê-lo o mesmo número de vezes para que ele desapareça da lista de chamadas.
Um pouco de detalhes internos
Por trás dos panos, um evento em C# é um campo-delegate (ou uma lista de delegates), e o operador += na prática chama Delegate.Combine, enquanto -= chama Delegate.Remove. O objeto que se inscreve no evento vira parte do grafo de referências. Por isso uma inscrição esquecida = vazamento de memória.
2. Vazamentos de memória via eventos: como isso funciona
Situação clássica
class Window
{
public event EventHandler Updated;
public void SimulateUpdate()
{
// Simulação: notificamos todos os subscribers
Updated?.Invoke(this, EventArgs.Empty);
}
}
class SettingsForm
{
public void OnWindowUpdated(object sender, EventArgs e)
{
Console.WriteLine("SettingsForm reage à atualização da janela");
}
}
Vamos passo a passo:
var window = new Window();
var settingsForm = new SettingsForm();
window.Updated += settingsForm.OnWindowUpdated;
window.SimulateUpdate(); // SettingsForm reage
// O usuário fechou o formulário. Perdemo-nos todas as referências a ele:
settingsForm = null;
// Mas o objeto SettingsForm NÃO será coletado pelo GC enquanto window estiver vivo,
// porque window.Updated ainda mantém referência ao método OnWindowUpdated,
// e portanto ao próprio objeto SettingsForm.
O que fazer?
Cancelar a inscrição:
// Para isso precisamos manter referência ao handler ou ao objeto:
window.Updated -= settingsForm.OnWindowUpdated;
settingsForm = null; // Agora o objeto pode ser recolhido
Tabela: quem mantém referência a quem
| Ação | Quem mantém referência | É possível liberar memória? |
|---|---|---|
| Inscrição no evento (+=) | Publisher para o subscriber | Não, enquanto o publisher estiver vivo |
| Cancelamento (-=) | Não | Sim, depois de remover todas as referências externas |
| Sem inscrição | Não | Sim |
3. Como organizar corretamente o cancelamento
Remoção explícita do handler
Isso pode ser feito, por exemplo, no momento do fechamento da janela ou do formulário:
class SettingsForm
{
private readonly Window _window;
public SettingsForm(Window window)
{
_window = window;
_window.Updated += OnWindowUpdated;
}
public void Close()
{
_window.Updated -= OnWindowUpdated; // cancelamos a inscrição!
// aqui vai o código para fechar (por exemplo, Dispose, GC.SuppressFinalize, etc.)
}
public void OnWindowUpdated(object sender, EventArgs e)
{
// Tratamento do evento
}
}
Se o SettingsForm for destruído via botão "fechar", é importante não esquecer de chamar o método onde a desinscrição acontece (por exemplo, Close()).
Usando a interface IDisposable
Para objetos mais complexos que se inscrevem em eventos e controlam seu próprio ciclo de vida, é conveniente implementar a interface IDisposable. No método Dispose() você faz todas as desinscrições necessárias.
class SettingsForm : IDisposable
{
private readonly Window _window;
public SettingsForm(Window window)
{
_window = window;
_window.Updated += OnWindowUpdated;
}
public void OnWindowUpdated(object sender, EventArgs e)
{
// ...
}
public void Dispose()
{
_window.Updated -= OnWindowUpdated;
// Aqui também liberamos outros recursos
}
}
Agora o SettingsForm pode ser usado dentro de um bloco using, chamar explicitamente Dispose() ou automatizar a liberação de recursos (por exemplo, via GC.SuppressFinalize em tipos finalizáveis).
4. Interação com expressões lambda: perigos e dicas
Se você se inscrever em um evento usando uma expressão lambda, mas não salvar a lambda em uma variável, você não vai conseguir cancelar a inscrição!
// Inscrição — lambda anônima
window.Updated += (s, e) => Console.WriteLine("Lambda chamada!");
// E agora como cancelar? — Não tem como!
window.Updated -= (s, e) => Console.WriteLine("Lambda chamada!"); // Isso é outro delegate!
O que fazer?
Guarde a lambda em uma variável-delegate:
EventHandler handler = (s, e) => Console.WriteLine("Lambda chamada!");
window.Updated += handler;
// ... agora dá pra cancelar!
window.Updated -= handler;
5. Nuances úteis
Particularidades do ciclo de vida de objetos e eventos
Outro problema comum são referências cruzadas via eventos entre dois objetos "de longa duração". Por exemplo, uma janela inscrita no evento da outra, ambos usados ativamente, não são removidos — e a memória só cresce.
Recomendação: Procure sempre saber quem está inscrito em quem e quando precisa cancelar. Se a inscrição tem o mesmo ciclo de vida do publisher — beleza. Se o subscriber pode viver menos que o publisher, implemente cancelamento explícito.
Regra universal: "Se você se inscreveu — cancele!"
- Para publishers de longa duração (por exemplo, globais, singletons, janelas principais) — sempre implemente cancelamento nos subscribers.
- Para objetos temporários (por exemplo, notificações únicas ou eventos onde o subscriber vive mais que o publisher) — dá pra relaxar, mas ainda assim fique de olho no contexto.
- Use abordagens como WeakEvent (eventos fracos) ou frameworks específicos se não quiser gerenciar cancelamentos manualmente.
6. Erros típicos ao trabalhar com cancelamento
Cancelamento sem sucesso: o método-handler precisa ser o mesmo
É muito importante que ao cancelar você passe exatamente o mesmo handler que foi usado na inscrição. Caso contrário o cancelamento não vai funcionar.
Errado:
window.Updated += settingsForm.OnWindowUpdated;
// ...
window.Updated -= new SettingsForm().OnWindowUpdated; // Não vai funcionar! É outra instância e outro delegate!
Certo:
window.Updated -= settingsForm.OnWindowUpdated;
Se na inscrição foi usada uma lambda anônima sem salvar a referência ao delegate, você não consegue cancelar, porque isso sempre cria uma outra instância de delegate:
// Inscrição
window.Updated += (s, e) => Console.WriteLine("Lambda!");
// Tentativa de cancelamento — não vai funcionar!
window.Updated -= (s, e) => Console.WriteLine("Lambda!");
"Inscrição" esquecida
Muitas vezes o cancelamento simplesmente é esquecido, especialmente quando o subscriber vive mais que o publisher ou quando o dev não entendeu totalmente como eventos funcionam. Como consequência, objetos-subscribers ficam na memória por mais tempo que deveriam, causando vazamentos de memória e problemas de performance.
GO TO FULL VERSION