Olá pessoal. Tudo certo?
Eventos são uma abstração extremamente inteligente e poderosa. A sintaxe oferecida pelo C# para suporte de eventos é bela e simples de entender. Entretanto, tem algumas armadilhas importantes.
Considere o código que segue:
using System; namespace EventLeakDemo { internal class Program { private static void Main() { Console.Title = "Walking dead objects"; var s = new Source(); Foo(s); // GC.Collect(0); // s.RaiseMyEvent(); } static void Foo(Source s) { var c = new Consumer(s, "dead object"); } } internal class Source { public event EventHandler MyEvent; public void RaiseMyEvent() { EventHandler eh; lock (this) { eh = MyEvent; } if (eh != null) eh(this, EventArgs.Empty); } } internal class Consumer { private readonly string _id; public Consumer(Source s, string id) { _id = id; s.MyEvent += OnMyEvent; } private void OnMyEvent(object sender, EventArgs e) { Console.WriteLine("Event was listened by '{0}'", _id); } } }
Nesse breve exemplo, temos uma classe que “gera” eventos e outra que os consome. Na inicialização, criei uma instância da classe que gera e, depois, criei em um escopo delimitado uma instância da classe que consome.
Em primeira vista, o evento disparado pela “Source” jamais seria escutado por “Consumer”, certo? Afinal, a instância de Customer deveria morrer e ser coletada antes do evento ser disparado, concorda? Errado! Quando assinamos um evento, criamos uma referência direta de quem está “disparando eventos” para quem está “escutando”. Logo, a instância de “Customer” não é coletada pois ainda possui uma referência (em “Source”).
Veja:
A solução direta para o problema acima é cancelar a assinatura dos eventos de “customer” antes desse objeto ser descartado. Entretanto, esse tipo de cuidado é extremamente passível de falhas.
Abaixo uma solução mais “elaborada” e “a prova de enganos”.
using System; namespace WeakEvent.SimpleEventWrapper { internal class Program { private static void Main() { Console.Title = "Walking dead objects"; var s = new Source(); Foo(s); // System.GC.Collect(0); // s.RaiseMyEvent(); } static void Foo(Source s) { var c = new Consumer(s, "dead object"); } } internal class Source { public event EventHandler MyEvent; public void RaiseMyEvent() { EventHandler eh; lock (this) { eh = MyEvent; } if (eh != null) eh(this, EventArgs.Empty); } } internal class Consumer { private readonly string _id; private MyEventWrapper _myEventWrapper; public Consumer(Source s, string id) { _id = id; //s.MyEvent += OnMyEvent; _myEventWrapper = new MyEventWrapper(s, this); } public void OnMyEvent(object sender, EventArgs e) { Console.WriteLine("Event was listened by '{0}'", _id); } } internal class MyEventWrapper { private readonly WeakReference<Consumer> _wr; private readonly Source _source; public MyEventWrapper( Source source, Consumer consumer ) { _wr = new WeakReference<Consumer>(consumer); _source = source; source.MyEvent += OnMyEvent; } public void OnMyEvent( object source, EventArgs e ) { Consumer c; if (_wr.TryGetTarget(out c)) { c.OnMyEvent(source, e); } else { Deregister(); } } public void Deregister() { _source.MyEvent -= OnMyEvent; } } }
Executando …
A ideia é deixar de assinar os eventos diretamente criando um “wrapper” que mantém uma “referência fraca” para “Consumer”. Assim, a coleta poderá ocorrer naturalmente. Pegou?
Esse conceito fundamental está no “core” do WPF. Mais adiante, voltarei a abordar essa estratégia mostrando abordagens mais elegantes (e menos trabalhosas).
Era isso.