CodeGym /Cursos /C# SELF /Erros comuns com delegates e events

Erros comuns com delegates e events

C# SELF
Nível 54 , Lição 0
Disponível

1. Introdução

Trabalhar com delegates e events em C# é agradável e conveniente — a linguagem faz muita coisa por você. Contudo, por trás dessa fachada há vários obstáculos: leaks invisíveis de memória, bugs estranhos por inscrição dupla, dessíncronia de handlers e até exceções súbitas durante a dispatch de notificações. Delegates e events são poderosos, mas exigem cuidado com o ciclo de vida dos objetos, entendimento de threads e de como exatamente funcionam chamadas e desinscrições. Se seus events funcionam "quase sempre", mas às vezes não disparam ou causam erros estranhos — você não está sozinho! Vamos ver onde até programadores experientes mais erram e como evitar isso.

Tabela de erros principais

Erro Consequência Como evitar
Inscrição dupla Handler é chamado múltiplas vezes Acompanhar inscrições, remover antes de adicionar
Não desinscrever (vazamento de memória) Subscribers ficam na memória, “zumbis” Sempre desinscrever, usar IDisposable
Exceção no handler Handlers restantes não serão chamados try/catch nos handlers ou iterar manualmente
Modificar assinantes durante o event Perda ou duplicação de chamadas Iterar sobre uma cópia dos handlers (GetInvocationList())
Chamar evento quando MyEvent == null NullReferenceException Checar por null, usar ?.Invoke
Assinatura do handler não bate Erro de compilação Verificar assinatura
Chamar evento fora da classe publisher Erro de compilação Chamar só via OnEventName
Eventos static no lugar errado Mix de assinaturas Não fazer static sem necessidade
Problemas de closure com lambdas Valores inesperados Fazer cópia da variável

2. Inscrição múltipla e chamadas múltiplas

Essência do erro

Se você inscrever o mesmo handler várias vezes em um mesmo event, cada += adiciona seu método na fila do delegate. Como resultado, o handler será chamado tantas vezes quanto foi adicionado.

Como isso aparece?

Imagina que você tem um botão e um handler de click:

Button btn = new Button();
btn.Click += OnButtonClick; // Vamos inscrever
btn.Click += OnButtonClick; // E aqui temos inscrição duplicada!

Agora, a cada clique o método OnButtonClick será chamado duas vezes. Se dentro do handler você, por exemplo, atualiza um contador ou escreve no log, verá resultados dobrados.

Como consertar?

Normalmente a inscrição duplicada acontece por violação da estrutura do código — por exemplo, se o += está em um método que é chamado várias vezes (em diferentes ciclos de vida da form).

  • Monitore onde a inscrição acontece.
  • Não coloque += em etapas do ciclo de vida que podem ser chamadas múltiplas vezes.
  • Às vezes é útil garantir uma inscrição “única” — antes de adicionar o handler, remova-o:
myEvent -= MyHandler; // Remove só por segurança
myEvent += MyHandler; // Depois inscreve de novo

Isso é seguro: se o handler ainda não existia, o -= não faz nada.

3. Subscriber zumbi

Essência do erro

Se um subscriber se inscreve em um publisher de longa vida e não se desinscreve, o GC não vai coletá‑lo: o publisher ainda mantém referência ao delegate do handler, logo — ao objeto subscriber. Resultado — leak de memória.

Exemplo típico

public class TemporaryPopup : IDisposable
{
    private Window _hostWindow;
    public TemporaryPopup(Window window)
    {
        _hostWindow = window;
        _hostWindow.Closed += OnHostClosed;
    }

    private void OnHostClosed(object sender, EventArgs e)
    {
        // ...
    }

    public void Dispose()
    {
        _hostWindow.Closed -= OnHostClosed; // Não esqueça de desinscrever!
    }
}

Se você esquecer de chamar Dispose() ou não o invocar, mesmo removendo todas as referências ao TemporaryPopup, o objeto não será coletado — a janela ainda referencia seu handler.

Como evitar?

  • Implemente IDisposable nos subscribers se o ciclo de vida deles for mais curto que o do publisher.
  • Use o padrão using ou chame explicitamente Dispose():
using (var popup = new TemporaryPopup(mainWindow))
{
    // ...
} // Aqui Dispose é chamado automaticamente

Em apps GUI — desinscreva ao fechar a janela/form (por exemplo, nos handlers de fechamento ou no Dispose() da form).

4. Tratamento de exceções dentro dos handlers

Essência do erro

Quando um event invoca dezenas de handlers e um deles lança uma exceção, os outros handlers não serão executados.

Demonstração

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent?.Invoke(this, EventArgs.Empty);
}

Se um dos métodos inscritos em MyEvent lançar uma exceção, os demais não serão chamados — a cadeia é interrompida.

Como lidar com isso?

  • Nos handlers trate exceções localmente (try/catch) ou deixe explicitamente propagar.
  • Em cenários complexos — itere manualmente pelos subscribers e isole erros de cada um:
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
    try
    {
        handler.DynamicInvoke(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        // Logging, recuperação de emergência
    }
}

5. Modificar a lista de assinantes durante a dispatch

Essência do erro

Se um handler se desinscreve ou inscreve outros handlers enquanto o evento está sendo enviado, isso pode corromper a ordem de chamadas: alguns handlers podem ser pulados ou chamados duas vezes.

Como evitar?

  • Não modifique assinaturas a partir de handlers.
  • Se precisar, itere sobre uma cópia da lista de delegates:
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
    handler(this, EventArgs.Empty);
}

6. Confusão com valor null do delegate (sem assinantes)

Essência do erro

Se ninguém está inscrito no event, seu delegate é null. Chamar sem checar causa NullReferenceException.

Exemplo (ruim)

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent(this, EventArgs.Empty); // Se não houver assinantes — exceção!
}

Como fazer certo?

  • Use chamada segura: MyEvent?.Invoke(this, EventArgs.Empty).
  • Ou use o clássico padrão thread‑safe: copie o delegate para uma variável local e invoque-a.

7. Misturar delegates/events com assinaturas diferentes

Essência do erro

Delegates são estritamente tipados. Mismatch entre a assinatura do método handler e do delegate do event é erro de compilação.

Exemplo

public event EventHandler<string> TextChanged;

void WrongHandler(object sender, int number) { /* ... */ }

TextChanged += WrongHandler; // Erro de compilação!

Use os delegates padrão EventHandler e EventHandler<T>, e verifique o perfeito alinhamento das assinaturas.

8. Tentar invocar evento fora da classe-publisher

Essência do erro

event é um delegate encapsulado: código externo não pode invocá‑lo diretamente (só pode adicionar/remover handlers).

Exemplo

public class MyPublisher
{
    public event EventHandler SomethingHappened;
}

var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // Erro!

Como proceder?

A invocação deve ser feita via método protegido/público do publisher — normalmente OnEventName. De fora — apenas +=/-=.

9. Erros com accessors add e remove

Essência do erro

Custom accessors para events permitem controlar a inscrição, mas é fácil quebrar a ordem correta de chamadas ou a thread‑safety.

Exemplo

public event EventHandler MyEvent
{
    add { /* ... */ }
    remove { /* ... */ }
}

Se não tiver certeza — use a implementação padrão de events. Se implementar manualmente, consulte a documentação e cuide da sincronização.

10. Acesso a events de contextos static e instance

Essência do erro

É fácil declarar por engano um event static quando ele deveria ser por instância. Aí todos os objetos compartilham a mesma fila de subscribers.

Exemplo

public static event EventHandler GlobalEvent; // Oops!
// Instâncias perdem individualidade, inscrições se misturam

Como evitar?

Faça um event static só quando realmente for um nível global (por exemplo, log global). Na maior parte dos casos, mantenha encapsulação por instância.

11. Problemas com captura de variáveis em lambdas

Essência do erro

Lambdas capturam variáveis por referência. Em loops isso geralmente leva ao efeito do “último valor”.

Exemplo

for (int i = 0; i < 5; i++)
{
    button.Click += (s, e) => Console.WriteLine(i);
}
// Todos os handlers vão imprimir "5"

Como fazer certo?

for (int i = 0; i < 5; i++)
{
    int copy = i; // Cópia local
    button.Click += (s, e) => Console.WriteLine(copy);
}

12. Mistura de referências fracas e fortes: Advanced “Weak Events”

Em apps grandes (por exemplo, WPF) usa‑se o mecanismo de “weak events”, onde o publisher mantém uma weak reference ao subscriber para não impedir o GC. Weak events ajudam contra leaks, mas o subscriber pode ser coletado e deixar de receber o evento.

Detalhes: Weak Event Patterns (MSDN)

13. Falta de padrão de naming e assinaturas de events

Essência do erro

Siga assinaturas e nomes padrão: events no passado (Changed, Closed, Completed), e args derivando de EventArgs.

Exemplo “errado”:

public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;

Exemplo “certo”:

public event EventHandler<EventArgs> SomethingHappened;

Para seus events, quase sempre use EventHandler ou EventHandler<T>. Seus colegas (e o você do futuro) vão agradecer.

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