CodeGym /Cursos /C# SELF /Cancelar inscrição de eventos (

Cancelar inscrição de eventos ( -=) e vazamento de memória

C# SELF
Nível 53 , Lição 1
Disponível

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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION